Expo and React Native API IDOR: Testing the Backend Behind the App
Expo apps feel like a UI problem. They are not. Almost every screen is a thin client over JSON. If the API trusts a numeric userId from the body because the mobile UI only shows "your" data, you have classic broken object-level authorization with a nicer font.
Pull the real surface
Run the app through a proxy on a device or emulator you control. Record:
- Base URL and API version prefixes (
/api/v2, Supabase REST, Firebase callable URLs) - Auth header shape: bearer JWT, Expo push token mistaken for auth, or cookie jars in WebViews
- Identifiers in paths:
/users/1842/orders,/profiles/{uuid}, GraphQLnode(id:)
JavaScript bundles from Expo updates can leak route maps and even staging hosts. Download the update manifest when in scope and grep for https:// and path templates. Do not dump secrets from unrelated tenants; stick to what the client already ships. Hermes bytecode is annoying—often the plaintext strings for paths still survive; use them as hints, then confirm live.
EAS Update channels sometimes publish a "preview" bundle with verbose logging or a staging API host. If that host shares credentials or databases with production, say so carefully and keep tests on accounts you own.
Swap IDs like a web hunter
Create two accounts—A and B—on the mobile client. Capture A's request to fetch a resource. Replay it with A's token but B's object id. Then try B's object with A's token on write methods: PATCH, DELETE, POST /transfer.
Mobile quirks to try:
- Integer IDs hidden behind UUIDs in the UI but still accepted by the API
- Batch endpoints (
ids[]=) that skip per-object checks - Image or receipt uploads keyed only by filename
- GraphQL mutations where the screen sends
ownerIdfrom AsyncStorage - Optimistic UI calls that fire twice—second request may hit a different authz branch
I've seen Expo apps store user.id in AsyncStorage and resend it on every mutation "for convenience." If the server honors that field over the JWT subject, rewrite it. Secure Store helps confidentiality on device; it does nothing if the API is gullible.
Push tokens, deep links, and offline queues
Confirm that Expo push tokens cannot pull another user's notifications inbox. Confirm deeplinked /invoice/INV-100 screens call an API that re-checks ownership rather than rendering whatever the link says. Offline mutation queues in libraries like Redux Persist or custom NetInfo flush logic sometimes replay stale bodies after account switch—log out, log in as B, and watch whether A's queued PATCH still fires with A's bearer.
File uploads deserve a dedicated pass. Content-Type confusion and path traversal in object keys show up when the mobile client sends a client-chosen filename straight to S3 or Supabase Storage. Pair that with an IDOR on the metadata row and you can read another user's receipt.
Write it for triage
Report with two raw HTTP exchanges, redacted tokens, and the exact field you changed. Severity follows data: PII and payment methods high; public profile display names low. Mention Expo/React Native only as the client that led you to the API—the fix sits on the server. Derive user from the session, authorize each object, and treat the mobile app as hostile input. OWASP API1 remains the right vocabulary for the title line.