Peloton exposed private data from 4.4 million users through an endpoint that never checked who was asking. The vulnerability required zero exploit knowledge — just a different numeric ID in the URL. The full fix took 4 months and only arrived after press coverage.
REST and GraphQL APIs fail systematically on the same 3 tests — preceded by GraphQL introspection as a reconnaissance step that reveals which mutations to attack: object-level authorization, mass assignment, and rate limiting. A practitioner can run all three against any endpoint in under an hour with curl and a proxy.
BOLA Is #1 Because Authorization Checks Are Not Automatic
BOLA has held the #1 spot on the OWASP API Security Top 10 in 2019 and again in 2023 for the same reason: frameworks handle authentication but leave authorization as the developer's responsibility. Developers consistently leave that check out of production code.
In January 2021, Pen Test Partners found that GET /api/ride/{id}/details returned workout stats, location, age, gender, and privacy settings without any authentication. The endpoint exposed data from 4.4 million Peloton accounts to any request with a valid ID in the URL. Partially fixed in February 2021, when data was still accessible to any authenticated user. The full fix only arrived in May after press coverage.
Salt Security 2024: 95% of organizations experienced an API security incident in production, and 23% resulted in a confirmed breach. Authorization failures dominate documented attack categories. BOLA remaining at #1 through two OWASP cycles signals that the industry has not solved the structural problem.
The test is 1 request. Make GET /api/orders/1001 authenticated as user A, then swap the ID to /api/orders/1002, which belongs to user B. A 200 response with B's data confirms BOLA unambiguously.
The failure mechanism is consistent. The ORM validates the JWT token, confirms the user exists in the database, and returns the requested object. Checking whether that object belongs to the authenticated user is additional code that most frameworks do not generate by default and must be written explicitly for each endpoint.
Every PATCH Endpoint Is Potential Privilege Escalation Until Proven Otherwise
Mass assignment exists because web frameworks bind all received JSON fields to internal models by default. Sending isAdmin, role, or verified in a PATCH is silently accepted in most implementations without an explicit allowlist of permitted fields.
CVE-2025-15602 (verified against NVD) in Snipe-IT documents the complete pattern. An authenticated low-privilege user sent PATCH /api/v1/users/{id} with additional restricted fields, modified the Super Admin's email, and triggered a password reset to take over the account. Fixed in version 8.3.7.
CVE-2026-44832 (verified against NVD) documents the next iteration of the same mistake in the same project. The API controller was updated to remove the superuser field from PATCH but missed removing the admin field. A user with only users.edit permission sent permissions[admin]=1 and gained full administrator access. Fixed in version 8.4.1.
OWASP API Security 2023 ranked Mass Assignment at #3, up from #6 in 2019. The climb reflects the proliferation of APIs with automatic binding and the absence of explicit field sanitization.
The correct defense is an allowlist in the controller, not a blocklist. Blocklists fail when new fields are added to the model without updating the blocklist — exactly the pattern that produced two consecutive CVEs in Snipe-IT.
The test: intercept a PATCH /profile with a proxy and add "role":"admin","isAdmin":true,"verified":true to the JSON body. If the server echoes any of those fields in the response, the field was accepted and persisted to the database.
GraphQL Introspection Publishes the API's Internal Surface to Any Attacker
Enabling introspection on production GraphQL APIs hands the attacker the complete schema in 1 query, before any exploitation begins. Apollo, graphql-js, Hasura, and all major libraries enable introspection by default. Disabling it requires an explicit opt-out configuration in each framework.
HackerOne report #1132803 documents the basic case. A __schema query against a production GraphQL endpoint returned the full schema, including internal query types and mutations not publicly documented. Rated as a valid medium severity finding by the bug bounty program.
The Shopify HackerOne #2886723 case shows the full chain. An IDOR in the GraphQL queries BillingDocumentDownload and BillDetails paid a $5,000 bounty. Those queries did not appear in the public documentation but were visible via introspection. The authorization vulnerability was only discovered because the schema was exposed in production.
The intel.mago.team tool enumerates API surface including undocumented GraphQL endpoints, demonstrating that this reconnaissance step is automatable in minutes by any attacker with access to the endpoint.
The test: POST {"query":"{ __schema { queryType { name } types { name fields { name } } } }"} to the GraphQL endpoint. A 200 response with type names confirms active introspection in production. Fix: introspection: false in production, playground restricted to the development environment.
Introspection is not the fourth test — it is the reconnaissance step that reveals which mutations to use in the alias attack described next.
GraphQL Aliases Let One HTTP Request Do the Work of a Thousand
A GraphQL alias attack sends 100 independent operations in 1 HTTP request. The rate limiter counts HTTP requests, not operations. A "10 requests/min" limiter becomes 1,000 operations per minute when aliases are available without query complexity controls.
PortSwigger 2023 documented the technique: a POST to /graphql with 100 login mutations via aliases results in 100 authentication attempts counted as 1 request by any rate limiter at the HTTP layer. Aliasing is a feature of the GraphQL specification. Every compliant server supports it by default.
Checkmarx detailed the real-world impact: credential stuffing at 10,000 operations per minute through a limiter configured for 10 requests per minute (Checkmarx Research, GraphQL Batching Attacks, 2023). The most critical surface is where the same sensitive operation exists on a REST endpoint with rate limiting and on a GraphQL schema without equivalent controls. OTP validation, password reset, and login are the most common targets in this pattern.
The test: send a query with a0:login(email:"t@x.com",password:"a") a1:login(email:"t@x.com",password:"b") repeated through alias a99 and observe how many aliases return a response versus trigger a block. The fix requires rate limiting by query complexity with alias counting and maximum depth, not by HTTP request count.
X-Forwarded-For Resets IP-Based Rate Limiters in REST APIs
Any REST API that uses client-supplied proxy headers to track rate limiting can be bypassed by rotating 1 header value. A per-IP limiter becomes unlimited throughput for any client that controls its own request headers.
Mastodon GHSA-c2r5-cfqr-c553 documents the mechanism. rack-attack rate limits by IP, but the Rails RemoteIp middleware trusts X-Forwarded-For from unvalidated clients. Setting X-Forwarded-For: 127.0.0.1 triggers rack-attack's loopback exemption and enables unlimited brute force on authentication endpoints. The vulnerability affects servers not sitting behind a proxy that rewrites or strips the header before it reaches the application.
The estimated impact: $40,000 in API credits stolen from a commercial platform via this single header bypass.
The surface extends beyond X-Forwarded-For. Frameworks check X-Real-IP, X-Originating-IP, X-Remote-IP, and X-Client-IP in priority order. Any of them can be the bypass vector if accepted without server-side validation. The correct protection is trusting only the TCP connection IP when the server is not behind a known trusted proxy.
The test: make 5 requests to a rate-limited endpoint until blocked, then add X-Forwarded-For: 1.2.3.4 to the next request. If the counter resets, the rate limiter trusts client-supplied headers and the control is ineffective.
The 3 tests produce a binary result in minutes: the API enforces the control or it does not. The same endpoints that fail BOLA tend to fail mass assignment because the root cause is the same. The API layer was built to move data, not to enforce policy. Running the tests before deploy is less a security maturity decision than a basic engineering one.
Top comments (0)