Hunting Bugs in SPAs: API-First Methodology That Finds Real Issues
Single-page apps train you to click through React routes. The bugs live in the JSON. Pretty URLs are cosmetics; /app/settings/billing is still PATCH https://api.target.com/v1/billing. Shift your proxy mindset from pages to resources.
This workflow assumes Burp (or ZAP) history is your map — not the left nav.
Step 1: Separate origins
Note the web origin and the API origin. Cookies, CORS, and tokens often differ. SPAs commonly store JWTs in localStorage and send Authorization: Bearer. Anything in localStorage is XSS-complete; anything in a cookie needs HttpOnly / Secure / SameSite review.
Watch CORS preflights. Over-permissive Access-Control-Allow-Origin plus credentialed responses is its own finding class when combined with an attacker's site.
Step 2: Build an endpoint inventory
Browse every role you have while intercept is off. Export history. Cluster by path prefix. Tag:
- Resource IDs in paths and bodies
- Admin-looking verbs (
impersonate,sudo,export) - Feature-flagged calls that fired once
- GraphQL
/graphqloperations (name them from the query)
For GraphQL, enumerate operation names from the frontend bundle if introspection is off. Introspection on is a gift — download the schema early (OWASP API Top 10 frames broken object auth as a top risk).
Step 3: Test like the UI does not exist
For each authenticated API call:
- Replay without token → should fail closed.
- Replay with a lower-privilege user → authZ.
- Swap object IDs across users/tenants → BOLA/IDOR.
- Add fields the UI never sends (
role,price,verified). - Change HTTP method and content-type slightly.
SPAs hide buttons; APIs often still accept the action. Mass assignment and hidden properties show up constantly in JSON PATCHes.
Step 4: Client-side trust borders
Read the bundle for role checks, “isAdmin” booleans, and client-only validations. Then violate them via Repeater. File uploads that check magic bytes only in JavaScript fail the same way.
Service workers and offline caches can serve stale security decisions — rarer, but note if an old worker keeps a privileged route alive.
Step 5: State and websocket channels
Many SPAs open sockets for notifications. Subscribe to channels with another user’s IDs. Race REST deletes against websocket presence. Token refresh endpoints deserve the same authZ attention as login.
Reporting habits for SPA bugs
Always include the raw API request, not a click path alone. Triagers reproduce faster from Repeater dumps. Name the broken control: BOLA, mass assignment, missing function auth — map to CWE-639 or CWE-862 as fits.
Third-party embeds inside the SPA (billing widgets, chat, analytics) bring extra origins into scope discussions. Capture their calls separately so you do not mix vendor issues with the core product API.
Lightweight daily loop
Map → inventory → two-account ID swaps → hidden field tampering → CORS/token storage notes. Skip chasing DOM XSS for a morning if the API still returns other people’s invoices; priority follows data.
The SPA is a skin. Hunt the API under it, and the same methodology scales from tiny React apps to giant GraphQL gateways.