Half a Billion Profiles Behind a User-Agent String
The API endpoint was public. The user IDs were sequential integers. The response contained everything — name, email, activity history, account metadata. And the only thing standing between an attacker and hundreds of millions of user profiles was a firewall rule that didn't apply to mobile traffic.
Finding the Door
The platform was massive. Tens of millions of daily active users. The kind of service people use without thinking about security — they trust it because everyone uses it. Their data lives there: personal information, learning history, usage patterns, email addresses.
The API was well-documented in some ways. Endpoints followed predictable patterns. User profiles lived at a path like:
GET /api/v1/users/{id}
Where {id} was a numeric identifier. Simple, sequential, starting from 1.
From a web browser, requesting another user's profile returned a sanitized response — limited public information, nothing sensitive. The Web Application Firewall sat between the browser and the API, enforcing rate limits and filtering requests that looked like enumeration attempts.
That was the perimeter defense. And it worked, as long as you played by its rules.
The Bypass
WAFs work by inspecting traffic and matching it against rules. These rules look at request patterns, rate of requests, IP addresses, and — critically — what kind of client is making the request.
Most WAF configurations focus on browser traffic. That's where the attacks traditionally come from. But mobile apps? They're often treated differently. Lower rate limits, different rule sets, sometimes no inspection at all. The logic is understandable: mobile apps are compiled binaries, harder to tamper with, and they're making API calls on behalf of authenticated users.
Except User-Agent headers aren't authenticated. They're just strings.
http
GET /api/v1/users/12345 HTTP/1.1
Host: api.platform.example.com
User-Agent: PlatformAndroid/8.0One header. That's all it took.
With a mobile User-Agent, the WAF stepped aside. No rate limiting. No request filtering. No enumeration detection. The request went straight to the API.
And the API responded with everything.
The Scale
The response wasn't sanitized for mobile clients. Where the web response returned limited public data, the mobile response returned the full profile: name, email address, account creation date, last activity, learning history, study sets, group memberships, profile metadata.
The IDs were sequential integers starting from single digits and going into the hundreds of millions. There was no randomization, no UUID, no obfuscation. User 1, user 2, user 3 — all the way up.
A simple loop:
for id in range(1, 500_000_000):
response = GET /api/v1/users/{id}
# Full profile data in every response
No authentication required. No rate limiting applied. No authorization check on whether the requester should see that specific user's data.
The theoretical exposure: every user account on the platform. Hundreds of millions of profiles, each containing personally identifiable information. All accessible to anyone who knew to change a single HTTP header.
Why This Happens
This is an IDOR — Insecure Direct Object Reference. The application exposes internal identifiers (user IDs) and doesn't verify that the requesting user is authorized to access that specific object.
But the more interesting question is: why was the WAF bypass there?
The answer is organizational. Web traffic and mobile traffic are often handled by different teams. The security team configures WAF rules for the web application. The mobile team builds their own API client. Somewhere in between, the assumption forms that mobile traffic is inherently more trustworthy because it comes from a "controlled" client.
That assumption is wrong. A User-Agent header is a string that anyone can set. An HTTP request is an HTTP request, regardless of which application generated it. There is no technical mechanism that prevents a script from pretending to be a mobile app.
The WAF was doing its job — protecting against the threats it was configured to detect. But it was configured with a blind spot. And that blind spot covered the entire mobile API surface.
The Defense
The fix is layered:
1. Authorization at the API layer. Every request to /api/v1/users/{id} should verify that the authenticated user has permission to view that specific profile. Not "is the user logged in" — "is the user allowed to see this record." This is the fundamental fix.
2. Remove sequential IDs from public-facing APIs. Use UUIDs or other non-enumerable identifiers. Even with proper authorization, sequential IDs telegraph the existence and approximate count of records.
3. Uniform WAF policies. Apply the same rate limiting, anomaly detection, and request filtering rules to all API traffic, regardless of User-Agent. The client identifier should never be a factor in security enforcement.
4. Response sanitization. Mobile and web clients should receive the same data filtering. If a field isn't meant to be public, strip it from all responses, not just browser responses.
The Takeaway
A Web Application Firewall is a network-level control. It operates on traffic patterns and request signatures. It is not — and was never meant to be — a replacement for authorization logic in the application itself.
When the WAF is the only thing preventing unauthorized data access, you don't have a security architecture. You have a single point of failure disguised as defense in depth.
The attack here wasn't sophisticated. It was a single HTTP header. The vulnerability wasn't exotic — it was a missing authorization check that exists on OWASP's Top 10 as the number one most common web application vulnerability.
Sometimes the biggest exposures come from the smallest oversights.