User accesses another user's data at the same privilege level by modifying a resource identifier in the request.
TL;DR
Horizontal privilege escalation is the most prevalent IDOR subtype. Both User A and User B hold the same role — both are regular users, both are paying customers, both are registered members. User A accesses User B's private resources by substituting B's resource identifier into a request authenticated as A. The server validates A's session correctly — the authentication check passes — but performs no second check to confirm that A's identity is permitted to access the specific resource B owns.
This subtype accounts for the majority of high-severity bug bounty IDOR reports. OWASP API Security Top 10 designates it as API1:2023 — Broken Object Level Authorization (BOLA), a position it has held since the list's 2019 inaugural edition. The OWASP API Security project estimates BOLA is present in approximately 40% of all API attacks.
The critical distinction from vertical escalation: no privilege gain occurs. User A does not become an admin. User A simply reads, modifies, or destroys data that belongs to B. The severity emerges from what that data contains — medical records, financial transactions, private messages, proprietary documents — and from the fact that a single vulnerable endpoint can be exploited across every user pair in the application.
The gold-standard confirmation methodology uses two independent sessions:
Setup:
token_A = authenticate(user_A) # User A session
token_B = authenticate(user_B) # User B session
resource_id = POST /api/documents # create resource as User B
with token_B → extract id # note the returned ID
Test:
GET /api/documents/{resource_id}
Authorization: Bearer {token_A} # request as User A
Confirm:
status == 200 AND body contains user_B's data → CONFIRMED BOLA
status == 403 OR 404 → protectedThe same pattern applies to write and delete operations. A read IDOR (CVSS 6.5) that escalates to write (modify B's data) or delete (destroy B's data) reaches CVSS 7.5–8.8:
# Read — IDOR confirmed
GET /api/messages/1338 HTTP/1.1
Authorization: Bearer <token_A>
# Write escalation — test PATCH/PUT with User B's resource ID
PATCH /api/messages/1338 HTTP/1.1
Authorization: Bearer <token_A>
Content-Type: application/json
{"content": "attacker-controlled content"}
# Delete escalation — test DELETE with User B's resource ID
DELETE /api/messages/1338 HTTP/1.1
Authorization: Bearer <token_A>| Variant | Technique | Impact |
|---|---|---|
| GET peer access | Substitute peer's resource ID with own session | Data read (PII, financial, medical) |
| Write peer IDOR | PUT/PATCH peer's resource ID | Data modification / integrity |
| Delete peer IDOR | DELETE peer's resource ID | Data destruction |
| Batch IDOR | Array of mixed user IDs in single request body | Mass exfiltration, rate-limit bypass |
| GraphQL node IDOR | query { node(id: "VXNlcjoxMzM4") { ... } } | All fields on any object type |
| GraphQL mutation IDOR | Mutation with peer's object ID | Write/delete on any mutation |
Bulk-fetch endpoints that accept arrays of IDs expose a particularly high-impact pattern. A single HTTP call can exfiltrate thousands of records while consuming only one request toward rate limits:
POST /api/documents/bulk-fetch HTTP/1.1
Authorization: Bearer <token_A>
Content-Type: application/json
{"document_ids": [1001, 1002, 1003, 5000, 9999, 50000]}If documents belonging to User B, C, or D are included in the response, this is a mass horizontal IDOR. Per-item authorization must be applied in the batch handler, not just at the endpoint level.
GraphQL Relay-spec implementations expose a global node(id:) interface that resolves any object by its global ID. Global IDs are typically base64-encoded type+ID pairs:
# Decode your own ID: VXNlcjoxMzM3 = base64("User:1337")
# Increment the numeric component and re-encode: User:1338
query {
node(id: "VXNlcjoxMzM4") {
... on User {
email
phoneNumber
privateData
}
}
}Individual field resolvers must verify ownership independently — gateway-level authentication that confirms a session exists does not confirm the session owns the requested object. Report HackerOne #2122671 ($12,500) exploited this exact pattern on the HackerOne platform itself, enabling deletion of any user's certifications via a GraphQL mutation.
CVE-2024-46528 — KubeSphere IDOR (CVSS 6.5, HIGH, September 2024) — KubeSphere 3.x (up to v3.4.1) and 4.x (up to v4.1.1) granted excessive permissions to the built-in authenticated GlobalRole — the role automatically assigned to every logged-in user. Low-privileged platform users could access cluster-level monitoring APIs, full user lists, and namespace resources across the cluster without any role elevation. The CVSS vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N reflects low-privilege network access with high confidentiality impact. Fixed in KubeSphere 4.1.3.
HackerOne #2122671 — HackerOne Platform ($12,500, GraphQL) — An authenticated researcher found a GraphQL mutation that deleted any user's certifications and professional licenses by submitting the target user's certification id. The caller's identity was validated (authentication passed) but not compared against the certification's owner (authorization absent). The HackerOne platform itself — which runs bug bounty programs — was vulnerable to horizontal IDOR via its own GraphQL API.
McDonald's McHire (July 2025, 64 million records) — Researcher Ian Carroll discovered that the lead_id parameter on /api/lead/cem-xhr was a sequential integer. Any registered job applicant could access any other applicant's chat transcript — name, phone number, email, shift preferences, and personality assessment results — by decrementing or incrementing the lead_id. A single API call with a manipulated ID returned a complete stranger's hiring interview record.
CVE-2025-1270 — Anapi h6web Platform (HIGH, 2025) — Authenticated users could modify the pkrelated parameter to access other users' records. Subsequent actions were executed with the impersonated user's privileges, chaining horizontal IDOR into vertical escalation — lateral movement via parameter manipulation.
GET /api/resource/{b_resource_id} with Authorization: Bearer {token_A}.node(id:).# Autorize (Burp Suite extension) — automatic cross-session replay
# 1. Set User B's session cookie as the "low-privilege session" in Autorize
# 2. Browse as User A — Autorize replays every request with User B's cookie
# 3. Compare response codes and body similarity
# 4. Red = likely vulnerable, green = likely protected
# nuclei — IDOR templates
nuclei -u https://target.com -t exposures/ids/ -H "Authorization: Bearer $TOKEN_A"BreachVex detects horizontal IDOR by replaying two authenticated identities against each other: when a primary and secondary session are available, User B's session is tested against User A's endpoints. Semantic JSON comparison extracts identity fields (id, user_id, email, username, account_id, owner, ownerId). Data overlap between sessions triggers a BOLA finding. A follow-up PUT/PATCH probe escalates a confirmed read BOLA to write-access BFLA (CRITICAL severity).
BFLA (Broken Function Level Authorization) is the write-access variant of horizontal IDOR. If an attacker can use User A's token to PATCH or DELETE User B's resource, the finding escalates from HIGH to CRITICAL — data integrity and account integrity are both compromised. Always test all HTTP methods, not just GET.
The structural defense: the ownership clause is part of the database query, not a conditional check that can be skipped:
# Django — VULNERABLE
def get_document(request, doc_id):
doc = Document.objects.get(id=doc_id) # any user's document
return JsonResponse(doc.to_dict())
# Django — SAFE
from django.shortcuts import get_object_or_404
def get_document(request, doc_id):
doc = get_object_or_404(Document, id=doc_id, owner=request.user)
return JsonResponse(doc.to_dict())
# get_object_or_404 returns 404 for both "not found" and "wrong owner"
# This prevents enumeration oraclesRole checks (RBAC) alone are insufficient for horizontal IDOR — both users pass the role check. Object-level ownership must be a separate, mandatory gate:
// Express — dual-layer authorization middleware
const authenticate = require('./middleware/authenticate'); // Layer 1: identity
const requireOwnership = (Model, idParam) => async (req, res, next) => { // Layer 2: ownership
const resource = await Model.findByPk(req.params[idParam]);
if (!resource) return res.status(404).json({ error: 'Not found' });
if (resource.userId !== req.user.id) {
return res.status(404).json({ error: 'Not found' }); // not 403
}
req.resource = resource;
next();
};
// Applied to every route handling user-owned resources
router.get('/api/messages/:id', authenticate, requireOwnership(Message, 'id'), getHandler);
router.patch('/api/messages/:id', authenticate, requireOwnership(Message, 'id'), patchHandler);
router.delete('/api/messages/:id', authenticate, requireOwnership(Message, 'id'), deleteHandler);Authorization in GraphQL must occur at the resolver level, not the gateway level. Every resolver receiving an object ID must verify ownership independently:
// GraphQL resolver — VULNERABLE (gateway auth only)
const resolvers = {
Query: {
document: (_, { id }, { user }) => Document.findById(id), // no ownership check
}
};
// GraphQL resolver — SAFE (resolver-level ownership)
const resolvers = {
Query: {
document: async (_, { id }, { user }) => {
const doc = await Document.findById(id);
if (!doc) return null;
if (doc.ownerId !== user.id) return null; // treat as not found
return doc;
}
}
};Horizontal privilege escalation occurs when a user accesses resources belonging to another user at the same privilege level — User A reads, modifies, or deletes User B's records by substituting B's identifier into an authenticated request. Both users hold the same role; the vulnerability is missing object-level ownership validation.
Horizontal escalation is lateral: User A accesses User B's data without gaining elevated privileges. Vertical escalation is upward: a regular user accesses admin functions or data. Horizontal is typically CVSS 6.5–7.5; vertical reaches 8.8–9.1 because it implies system-wide compromise.
BOLA (Broken Object Level Authorization) is the API-specific term for the same vulnerability, defined by OWASP API Security API1:2023. Horizontal IDOR in a REST or GraphQL API context is classified as BOLA. The fix is identical: ownership validation on every object-level API operation.
The two-token methodology creates two distinct user accounts, authenticates both, creates resources with one, then tests access to those resources using the other user's session token. If the second user's token retrieves the first user's private resource, horizontal IDOR is confirmed.
GraphQL resolvers handle individual field requests. Authentication typically occurs at the gateway level. If a resolver fetching user data validates that a session token exists but not that it matches the requested object's owner, any authenticated user can query any object ID. The node(id:) interface in Relay-style schemas is a common attack surface.
HackerOne report #2122671 found that a GraphQL mutation on the HackerOne platform itself allowed any authenticated user to delete any other user's certifications and licenses. The mutation's object ID was not validated against the caller's identity. Bounty: $12,500.
CVE-2024-46528 in KubeSphere 3.x and 4.x (CVSS 6.5) granted excessive permissions to the 'authenticated' GlobalRole. Any low-privileged authenticated user could access cluster-level monitoring data, full user lists, and namespace resources belonging to other users — all operations intended for cluster-admin only.
Yes. Endpoints accepting arrays of IDs in a single request can exfiltrate thousands of records in one HTTP call, bypassing rate limiting. This pattern is common in bulk-export or batch-fetch API designs and is a distinct high-severity variant requiring explicit per-item authorization.