August 4, 2026
How I Found a $250 BOLA in a Government Transport App’s Real-Time API
I found a systemic Broken Object-Level Authorization (BOLA) vulnerability in a government transport application’s real-time GraphQL API…

By anshh.bohara
4 min read
I found a systemic Broken Object-Level Authorization (BOLA) vulnerability in a government transport application's real-time GraphQL API. Any authenticated user could read, inject messages into, and spoof events across other users' private channels — trip chat, push notifications, live location, and charge events — simply by supplying another user's identifier. The program paid $250.
— -
Recon & Discovery
The target was a mobile application offered through a private bug bounty program on a managed platform. The app is a government-backed transport service — think trip sharing, live vehicle tracking, road-pricing charges, and in-app chat between trip participants.
I started by pulling the APK and mining its JavaScript bundles and native code. The app turned out to be backed by an AWS AppSync GraphQL API — a publish/subscribe architecture where mutations push events and subscriptions deliver them in real-time over WebSocket, each keyed by a client-supplied identifier like tripId, userId, or cognitoUserId.
GraphQL introspection was enabled on the production endpoint, which gave me the full schema: every mutation, subscription, query, and their arguments laid out. That's where I spotted the pattern — nearly every real-time operation accepted a user-controlled identifier as the routing key, and the question became: does the server verify the caller actually owns that identifier?
— -
The Vulnerability
The answer was no — for most of the real-time layer.
I registered a free account through the app's own federated Google sign-in (Cognito-backed, no invitation or special access needed) and started testing.
Trip chat — read and inject (the primary issue)
I opened a WebSocket connection, subscribed to a tripId that did not belong to me, and published a message to it. The subscription was accepted without any participation check, and the message came back in full:
[attacker] subscribe messageInbox(tripId: "[VICTIM-TRIP-ID]") → start_ack (accepted — no ownership check)
[attacker] sendMessage(tripId: "[VICTIM-TRIP-ID]", message: "attacker-controlled-content") → 200 OK
[attacker] ← received: { messageInbox: { tripId: "[VICTIM-TRIP-ID]", message: "attacker-controlled-content" } }
Any authenticated user who knows a victim's tripId can silently read all chat messages on that trip and inject arbitrary messages into it.
It was systemic — not just trip chat
Continued testing confirmed the same missing ownership check across the entire real-time layer:
- Trip chat — Subscribe via messageInbox(tripId), mutate via sendMessage(tripId) → read + inject private messages — Push notifications — Subscribe via notifyUser(cognitoUserId), mutate via sendUserNotification(cognitoUserId) → inject arbitrary notifications with full attacker-controlled title and body — ERP charge events — Subscribe via refreshMockEvent(userId), mutate via updateMockEvent(userId) → spoof "you were charged $X" events — Live trip location — Subscribe via refreshUserTripLocation(tripId), mutate via updateUserTripLocation(tripId) → spoof a user's live location
The notification injection was particularly concerning — an attacker could push legitimate-looking in-app notifications like "Account suspended — tap to verify" or "Payment failed — update card details" directly into a targeted user's app. On a government transport platform, that's a credible phishing primitive.
— -
The Negative Control (Why This Is Real)
This is the part that separates a valid finding from a false positive. The API does enforce authorization on some operations — proving the unprotected ones are a genuine gap, not a globally-open API by design:
updateBroadcastMessage(mode: "x", data: "x") → "Unauthorized — Not Authorized to access updateBroadcastMessage on type Mutation"
subscription refreshUserTripLocation(tripId: "[ID]") → "Unauthorized — Not Authorized to access refreshUserTripLocation on type Subscription"
Authorization is enforced where it's configured. It's simply absent on the operations I reported. The server has the mechanism — it just wasn't applied consistently.
I also confirmed this wasn't a "200 with echoed ID" false positive (a common trap with BOLA testing). I used two separate accounts — Attacker (A) and Victim (V) — and proved bidirectional cross-user impact:
- V sends a private message on their trip. A receives it via subscription — confidentiality broken.
- A injects a message into V's trip. V receives it in their subscription — integrity broken, phishing possible.
Both accounts were independently registered through the app's own sign-in flow. A was never a participant of V's trip.
— -
Honest Scope & Limitations
I flagged these transparently in the report:
- Trip chat has no persistent storage — it's real-time only. Reading a victim's messages requires being subscribed during the trip. — For cognitoUserId-keyed channels (notifications), the identifier is the user's Cognito sub. I didn't find a way to enumerate it from a federated account, so notification injection requires obtaining the target's sub through other means. — The tripId barrier is weaker — it's distributed by design in shareable links (that's the feature's purpose), and the underlying model uses numeric/sequential identifiers, making enumeration practical.
No real user's data was accessed. All testing used my own two accounts with synthetic identifiers.
— -
Impact
An attacker with a free, self-registered account could:
- Read other users' private trip chat messages in real-time — Inject arbitrary messages into any user's trip chat (impersonation, phishing) — Push attacker-controlled notifications into any user's app (social engineering) — Spoof ERP road-pricing charge events and live trip locations
This is on a government transport application used for official services — road pricing, parking, trip safety sharing. The injection capabilities create a direct phishing channel into users' trusted government app interface.
— -
Timeline
- Day 0 — Submitted initial report: trip chat BOLA with PoC and negative controls — Day 3 — Program asked for clarification: "show the target user is impacted" — Day 3 — Replied with full two-account cross-user PoC (both directions proven) — Day 5 — Submitted supplement: escalated from trip-chat-only to systemic (4 channels) — Day 8 — Multiple rounds of follow-up with additional evidence (Burp captures, WebSocket packets) — Day 30 — Bounty awarded: $250
— -
Takeaways
For hunters:
- Mine the APK/JS first. The entire attack surface — every mutation, subscription, and their parameters — was visible in the client code and via introspection. I didn't need to guess endpoints. — Always run the negative control. Proving that sibling operations are protected is what makes the finding undeniable. Without it, the program can argue it's "by design." — Escalate laterally before submitting. I initially reported just the trip chat issue. Testing the full real-time layer before the supplement would have made a stronger first impression and possibly a higher payout. — Be honest about limitations. Transparently noting that notification injection requires a known sub actually strengthened the report — it showed I wasn't inflating severity.
For developers:
- Authorization must be per-resolver, not per-API. Having auth on updateBroadcastMessage but not sendMessage in the same API means the middleware exists — it just wasn't applied everywhere. Audit every resolver independently. — Don't use client-supplied identifiers as authorization boundaries. If a subscription is keyed by tripId, the server must verify the caller is a participant of that trip — not just Audit every resolver independently.
- Don't use client-supplied identifiers as authorization boundaries. If a subscription is keyed by tripId, the server must verify the caller is a participant of that trip — not just that they know the ID.
- Disable introspection in production. It's a free schema dump for anyone with a valid token.
— - All testing was conducted within the program's rules of engagement, using self-registered accounts and synthetic identifiers. No real user data was accessed.