IDOR (CWE-639, OWASP A01:2021) lets attackers access any user's records by swapping an object ID — the #1 cause of SaaS data breaches and 49% of all critical bug bounty findings.
TL;DR
WHERE owner_id = session.user_id — and PostgreSQL RLS as backstopAn Insecure Direct Object Reference (IDOR) occurs when an application uses user-controlled input — a URL parameter, request body field, or HTTP header — to access an internal object without verifying the requester's authorization to that specific object. The server confirms the user is authenticated but skips the second question: does this user own this record? Classified as CWE-639 (Authorization Bypass Through User-Controlled Key), IDOR is the most prevalent subtype of OWASP A01:2021 Broken Access Control.
IDOR differs from authentication bypass (which defeats the identity check entirely), from SQL injection (which manipulates the query structure), and from CSRF (which forges requests using the victim's browser). The vulnerability is purely in the authorization layer: valid credentials, invalid permission. It also differs from BOLA (Broken Object Level Authorization, OWASP API1:2023) only in scope — BOLA names the same flaw when it occurs in REST/GraphQL APIs, where endpoint-level authentication passes but the object returned belongs to a different user. BFLA (Broken Function Level Authorization, API5:2023) is the vertical variant: the missing check concerns the caller's role rather than resource ownership.
OWASP Top 10:2025 places Broken Access Control at position one — retained from 2021. The contributing dataset records 1,839,701 occurrences, 32,654 mapped CVEs, and a striking data point: 100% of applications tested showed some form of broken access control. Bug bounty data reinforces this — roughly 49% of all high and critical severity HackerOne findings are IDOR or access control related, with over 200 valid IDOR reports submitted monthly.
An application endpoint accepts an object identifier from the user. The server fetches the object using that identifier without joining ownership. User A supplies User B's ID; the server returns User B's data.
The attack proceeds in four steps:
A confirmed horizontal IDOR via sequential integer:
GET /api/users/1338/profile HTTP/1.1
Host: target.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...<user_1337_token>
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1338,
"email": "victim@corp.com",
"phone": "+1-555-0192",
"address": "123 Main St, Springfield"
}The identifier appears in four locations: URL path (/api/users/1338/), query string (?user_id=1338), request body ({"user_id": 1338}), and headers (X-User-ID: 1338). All four locations must be tested.
| Variant | Technique | Impact | Dedicated Page |
|---|---|---|---|
| Direct Object Reference | Sequential integer enumeration in URL/query/body | PII exfiltration, account data | /learn/idor-direct |
| Horizontal Privilege Escalation | Peer-level lateral access (same role, different user) | Cross-user data theft | /learn/idor-horizontal |
| Vertical Privilege Escalation (BFLA) | Low-privilege user calls admin endpoints or HTTP methods | Admin takeover, data destruction | /learn/idor-vertical |
| Indirect Object Reference | Opaque reference (filename, slug, hash) still reversible | File disclosure, account enumeration | /learn/idor-indirect |
| Mass Assignment | Privileged field injection into POST/PUT body | Role escalation, balance manipulation | /learn/idor-mass-assignment |
Direct Object Reference is the baseline: GET /api/orders/84921 with order_id=84922 retrieves the next user's order. Sequential integers — the default in PostgreSQL auto-increment and MySQL AUTO_INCREMENT — make this trivial. The Optus breach repeated this pattern 9.8 million times.
Horizontal privilege escalation (peer-level lateral access) accounts for the majority of high-severity bug bounty IDOR reports. The two-account methodology is the gold standard: authenticate as User A, create a resource as User B, then attempt to read/modify/delete it as User A.
Vertical privilege escalation (BFLA) targets functions rather than data. Three distinct vectors exist: (1) direct admin endpoint access (GET /api/admin/users with a regular user token); (2) HTTP method override bypass using X-HTTP-Method-Override: DELETE on frameworks that honor it (Rails, Django REST Framework, Spring); (3) API version downgrade — GET /api/v2/admin/users returns 403 but GET /api/v1/admin/users returns 200 because the legacy endpoint lacks the authorization middleware added in v2.
Indirect object references appear secure but are not. An invoice reference INV-2024-04521 enumerates to INV-2024-04522. A Base64-encoded identifier eyJ1c2VyX2lkIjoxMzM3fQ== decodes to {"user_id":1337} — re-encode with 1338. An MD5-hashed user ID is reversible via rainbow tables. Second-order (stored) IDOR is temporally separated: User A supplies another user's ID during registration; a later dashboard request fetches data using that stored ID without re-checking ownership.
Mass assignment injects privileged fields (role, is_admin, tenant_id, balance) into POST/PUT request bodies that frameworks bind directly to ORM objects. CVE-2024-7297 (Langflow, CVSS 8.8): PATCH /api/v1/users/<USER_ID> with body {"is_superuser": true} granted full super-admin access to any authenticated user.
The OWASP API Security Top 10:2023 separates IDOR into two distinct entries:
| Term | OWASP API Entry | CWE | Scope |
|---|---|---|---|
| BOLA — Broken Object Level Authorization | API1:2023 | CWE-639 | Object ownership — returns another user's data |
| BFLA — Broken Function Level Authorization | API5:2023 | CWE-285 | Privilege level — caller invokes disallowed functions |
BOLA differs from classic web-app IDOR in tooling implication: endpoint-level authentication checks (validating that a Bearer token is present) do not catch BOLA. The authorization failure is at the resolver level — a route that correctly verifies identity but does not enforce ownership on the object returned. In GraphQL, BOLA manifests when a node(id: "VXNlcjoxMzM4") query returns User 1338's data to User 1337's session, because the global ID resolver lacks per-caller ownership validation.
BFLA is not about data ownership but function authorization. A regular user calling DELETE /api/admin/users/5 when only admins may delete users is BFLA. The most effective automated test: probe all endpoints matching /admin|manage|settings|internal|backoffice|system|config|moderation|staff|superuser/ with a low-privilege session and check for 200 responses with substantive admin content.
Cross-tenant IDOR is the highest-impact variant in SaaS. A single missing tenant_id in a WHERE clause exposes an entire organization's data rather than one user's record. CVSS scores consistently reach 8.8–9.5. The blast radius multiplier is the number of records the tenant holds.
In multi-tenant architectures, four failure patterns repeat across incidents: (1) shared caching layers (Redis, Memcached) keyed by object ID without a tenant prefix; (2) background job processors that accept object_id from a queue without tenant_id validation; (3) search indexes returning results across all tenants because the query-level tenant filter was omitted; (4) API endpoints that accept tenant_id as a user-controlled parameter — attackers simply substitute a competitor's tenant ID. The mandatory pattern: derive tenant context exclusively from the authenticated session, never from request parameters. Every ORM query must include WHERE tenant_id = :session_tenant_id as a structural constraint.
GraphQL IDOR exploits the Relay specification's global ID pattern. A user ID is encoded as base64("User:1337") = VXNlcjoxMzM3. Decode your own, increment the trailing integer, re-encode: VXNlcjoxMzM4 accesses User 1338. The query:
query {
node(id: "VXNlcjoxMzM4") {
... on User { email privateData }
}
}HackerOne report #2122671 (HackerOne platform, $12,500 bounty): a GraphQL mutation accepted an object ID in its arguments without validating it against the caller's identity. Any authenticated user could delete any other user's certifications and licenses.
Batch queries amplify the blast radius: a single GraphQL request containing 1,000 aliased node(id: ...) queries retrieves 1,000 different users' data while generating only one authentication event. Gateway-level rate limiting on request count does not protect against this.
JWT-based systems are vulnerable when the application trusts a user_id claim embedded in the token without re-validating ownership against the database. Three attack vectors: (1) weak or guessable HS256 secret allows forging a token with an arbitrary user_id; (2) alg: none downgrade (if the server accepts it) removes signature verification entirely; (3) kid injection substitutes the validation key. The pattern — decode the JWT, modify user_id from 1337 to 1338, forge a new signature or exploit a signature bypass — converts a read-only account into access to any other account.
WebSocket connections maintain persistent state; IDOR occurs when a client subscribes to a resource channel (subscribe {order_id: 12345}) without the server verifying ownership. Persistent connections enable rapid, low-noise enumeration. Standard DAST tools do not replay WebSocket frames — manual testing or custom tooling is required.
In microservice architectures, service-to-service calls often use elevated trust assumptions: Service A forwards a user_id header to Service B, which trusts it without re-authenticating. An attacker who compromises Service A or can forge internal headers bypasses all downstream authorization. OWASP guidance: apply object-level ownership checks in internal service handlers with the same rigor as external API handlers. Use mTLS via a service mesh (Istio, Linkerd) to enforce identity at the network layer.
UUID v1 is derived from a timestamp and MAC address. An attacker who obtains any UUID from the application (from an API response, notification, or shared link) can estimate neighboring UUIDs within a time window — the "sandwich attack." CVE-2024-45719 (Apache Answer): password reset tokens generated with UUID v1 were brute-forceable by any attacker who knew the account creation timestamp, enabling account takeover without any authentication.
UUID v4 uses 122 bits of cryptographic randomness. Enumeration is infeasible. However, UUID v4 does not prevent IDOR when the attacker obtains a reference legitimately — from a shared document link, an API response that leaks peer IDs, or server logs an attacker can read. Authorization must be enforced server-side regardless of identifier scheme.
CVE-2025-27507 — ZITADEL Admin API IDOR (CVSS 9.0 Critical, 2025)
ZITADEL, a Go/gRPC identity platform, used org-level IAM permissions instead of system-level permissions on 12 Admin API HTTP endpoints. Authenticated non-admin users called /idps/ldap and /idps/ldap/{id} to modify instance LDAP configuration, redirecting authentication to a malicious server. Impact: full account takeover for all LDAP users on the instance. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N.
CVE-2024-46528 — KubeSphere IDOR (CVSS 6.5 High, 2024)
KubeSphere v3.4.1 and v4.1.1 granted excessive permissions to the authenticated GlobalRole (CWE-639). Low-privilege authenticated users accessed cluster-level monitoring data and full user lists without elevation — data meant for cluster-admin only. CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N. Fixed in KubeSphere 4.1.3.
CVE-2024-45719 — Apache Answer UUID v1 Predictable Token (2024) Apache Answer generated password reset tokens using UUID v1 (timestamp + MAC). Attackers who obtained any UUIDv1 from the application computed valid reset tokens for other accounts within a time window — a sandwich attack. Impact: account takeover without credentials.
CVE-2024-7297 — Langflow Mass Assignment (CVSS 8.8 High, 2024)
Langflow (LangChain-based LLM app builder) before v1.0.13: PATCH /api/v1/users/<USER_ID> accepted {"is_superuser": true} from any authenticated user. Full super-admin access granted — all flows, credentials, and infrastructure. Fixed in v1.0.13 by adding server-side field filtering.
CVE-2026-29056 — Kanboard Mass Assignment via Invite Path (High, 2026)
Kanboard ≤ 1.2.50: UserInviteController::register() passed all POST parameters directly to UserModel::create() without filtering the role field. Any email-invite recipient registered with role=app-admin in the form body, gaining full administrator access. The RCE chain followed via plugin installation. Fixed by unset($values['role']) before model creation.
McDonald's McHire AI Chatbot Breach (July 2025, 64 million records)
Researchers Ian Carroll and Sam Curry found sequential integer IDOR on the McHire internal endpoint /api/lead/cem-xhr: the lead_id parameter returned any applicant's full chat record without authentication checks. Exposed data: names, emails, phone numbers, chat histories, personality test outcomes, and session tokens for candidate impersonation. This is the largest known IDOR breach to date.
Optus Data Breach (September 2022, 9.8 million records)
A public REST API endpoint — /api/customers/{id} — used sequential integers with no authentication. The endpoint had been patched on the main domain but the fix was not applied to a secondary domain. The attack: GET /api/customers/1 through GET /api/customers/9800000, repeated 9.8 million times. Regulatory outcome: AUD $1.36M fine from ACMA.
HackerOne #2122671 — HackerOne Platform GraphQL IDOR ($12,500, 2024) A GraphQL mutation accepted object IDs without validating them against the caller's identity. Any authenticated user deleted any other user's certifications and licenses. Bounty: $12,500. The highest single IDOR payout in public disclosure at time of reporting.
HackerOne #415081 — PayPal Business Management IDOR ($10,500)
IDOR on /businessmanage/users/api/v1/users allowed adding secondary users to any business account via parameter manipulation. Bounty: $10,500.
IDOR testing requires at minimum two authenticated identities. Single-user testing cannot detect authorization failures.
X-User-ID, X-Account-ID, custom headers)Detection confirms IDOR when an authorization bypass is corroborated by independent signals: a status change from 403 to 200, the response echoing the target's identifier but not the attacker's own, distinct PII surfacing in an otherwise identical JSON structure, or multiple non-timestamp JSON fields diverging between two sessions.
Burp Suite Autorize (Barak Tawily): two-session replay engine that tests every intercepted request against a secondary session. Best DAST tool for IDOR by practitioner consensus.
AuthMatrix: defines an N-roles × M-endpoints access control matrix and tests all combinations. Optimal for applications with complex multi-role permission structures.
Semgrep AI IDOR detection (2025 beta): traces request.params[id] → db.find(id=id) flows and checks for ownership filters. Achieves 22% TPR on single-file flows; fails completely when authorization spans middleware or parent class decorators.
BreachVex detects IDOR through multi-session differential analysis: each endpoint is tested with primary and secondary authenticated sessions, ownership is extracted semantically from JSON responses (id, user_id, email, account_id), and findings are corroborated across multiple complementary signals (status-code bypass, ID echo, PII comparison, JSON field divergence, response-size delta) before a finding is raised. Write-capable cross-session access escalates to BFLA at CRITICAL severity.
The single most important rule: derive user identity from the authenticated session and include the ownership constraint in every database query as a structural requirement — not an optional check.
# VULNERABLE — authenticates but does not authorize
@router.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, user: User = Depends(get_current_user)):
invoice = await db.get(Invoice, invoice_id) # fetches ANY invoice
return invoice
# SAFE — ownership enforced at the query level
@router.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, user: User = Depends(get_current_user)):
invoice = await db.execute(
select(Invoice)
.where(Invoice.id == invoice_id)
.where(Invoice.owner_id == user.id) # ownership gate — structural
)
if not invoice:
raise HTTPException(status_code=404) # 404 not 403 — prevents existence oracle
return invoiceReturn 404, not 403, on unauthorized access. Returning 403 confirms the resource exists and enables enumeration.
RLS enforces authorization at the database engine level — the strongest available backstop. Even if application code forgets the WHERE clause, the database rejects the query. RLS cannot be bypassed by application-layer bugs.
-- Enable RLS on the table
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Create a policy: each user sees only their own rows
CREATE POLICY invoices_owner_isolation ON invoices
USING (owner_id = current_setting('app.current_user_id')::int);
-- Set the current user ID in your connection setup (FastAPI example):
-- await db.execute("SET app.current_user_id = :uid", {"uid": user.id})
-- Every subsequent query on this connection is automatically scoped.// Reusable authorization middleware — attach before any handler touching user-owned resources
const requireOwnership = (Model, idParam = 'id') => async (req, res, next) => {
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' });
}
req.resource = resource;
next();
};
// Applied before handler — impossible to reach handler without passing ownership check
router.get('/api/documents/:id',
authenticate,
requireOwnership(Document),
(req, res) => res.json(req.resource)
);# VULNERABLE — opaque reference but no ownership filter
def download_invoice(request):
ref = request.GET.get("ref")
invoice = Invoice.objects.get(ref=ref) # any user's invoice
return FileResponse(open(invoice.pdf_path, "rb"))
# SAFE — ownership enforced on the indirect identifier too
def download_invoice(request):
ref = request.GET.get("ref")
# get_object_or_404 returns 404 not 403 — prevents resource oracle
invoice = get_object_or_404(Invoice, ref=ref, owner=request.user)
return FileResponse(open(invoice.pdf_path, "rb"))from pydantic import BaseModel
from typing import Optional
# Only expose fields that are user-controllable
class UserUpdateRequest(BaseModel):
display_name: str
bio: Optional[str] = None
# is_admin, role, tenant_id: NOT declared — rejected regardless of what client sends
@router.patch("/users/me")
async def update_user(user: User = Depends(get_current_user),
data: UserUpdateRequest = ...):
# Pydantic schema acts as allowlist — extra fields raise 422 before handler runs
await db.users.update(id=user.id, **data.model_dump(exclude_unset=True))UUID v4 reduces enumeration risk but is not a substitute for authorization. Once a UUID is shared (collaboration link, API response, server log), an attacker with that reference can test it across sessions. Authorization must always be enforced server-side regardless of identifier type.
Use UUID v4 for externally exposed identifiers. Never expose auto-increment integers directly to API consumers. Never use UUID v1 for security-sensitive tokens (password reset, invite links) — its timestamp derivation makes it brute-forceable.
For complex permission structures, three models apply:
Integrate cross-user assertions into the test suite:
# Pytest example — run on every PR
def test_invoice_idor(client_a, client_b, invoice_b):
"""User A must not retrieve User B's invoice."""
response = client_a.get(f"/api/invoices/{invoice_b.id}")
assert response.status_code in (403, 404), (
f"IDOR detected: user_a retrieved invoice belonging to user_b "
f"(status={response.status_code})"
)This pattern catches authorization regressions before production deployment.
What is an Insecure Direct Object Reference (IDOR)? An IDOR vulnerability occurs when an application uses user-controlled input to access an internal object — database record, file, API resource — without verifying the requester's authorization to that specific object. Classified as CWE-639, it falls under OWASP A01:2021 Broken Access Control.
What is the difference between IDOR and BOLA? IDOR is the general web vulnerability term. BOLA (Broken Object Level Authorization, OWASP API1:2023) is the API-specific equivalent. BFLA (API5:2023) is the vertical variant where the missing check is on privilege level rather than ownership. All three describe the same authorization gap in different contexts.
Is IDOR still in the OWASP Top 10 in 2025? Yes. Broken Access Control (A01:2025) retains its position as the #1 risk. OWASP records 1,839,701 occurrences and notes 100% of tested applications contain some form of broken access control.
Can IDOR lead to account takeover? Yes. The common chain: IDOR on password reset endpoint → modify victim's email → trigger reset to attacker inbox → full account takeover. CVE-2025-27507 (ZITADEL, CVSS 9.0) demonstrates IDOR enabling instance-level takeover for all LDAP users.
Are UUIDs enough to prevent IDOR? No. UUID v4 prevents enumeration; it does not prevent access once a reference is obtained. UUID v1 is partially predictable via sandwich attacks (CVE-2024-45719). Authorization checks are mandatory regardless of identifier type.
How do you test for IDOR? Create two accounts. Record resources owned by account B. From account A, attempt access using B's identifiers across all HTTP methods and parameter locations. Use Burp Suite's Autorize extension to automate two-session replay.
How do you prevent IDOR?
Derive identity from session only. Include WHERE owner_id = session_user_id in every query. Return 404 (not 403) on unauthorized access. Use UUID v4 for externally exposed IDs. Enforce PostgreSQL RLS as a database-layer backstop. Write cross-user assertions in CI/CD.
How does Row-Level Security prevent IDOR? RLS enforces the ownership constraint at the database engine level. Even if application code omits the WHERE clause, the database rejects the query based on the active policy. Application-layer bugs cannot bypass RLS.
An IDOR vulnerability occurs when an application uses user-controlled input — a URL parameter, request body field, or header — to access an internal object (database record, file, API resource) without verifying that the requesting user is authorized to access that specific object. It is classified as CWE-639 and falls under OWASP A01:2021 (Broken Access Control).
IDOR (Insecure Direct Object Reference) is the general web vulnerability term. BOLA (Broken Object Level Authorization) is its API-specific equivalent, holding position API1:2023 in the OWASP API Security Top 10. Both describe missing authorization checks at the object level. BFLA (Broken Function Level Authorization, API5:2023) is the related variant where the missing check concerns privilege level rather than object ownership.
Yes. Broken Access Control — the parent category containing IDOR — is OWASP A01:2025, retaining its #1 position from 2021. OWASP reports 1,839,701 occurrences across contributing datasets, 32,654 related CVEs, and 100% of tested applications showing some form of broken access control.
Yes. The most common chain: IDOR exposes a password-reset token or email-change endpoint for another user → attacker modifies the victim's email → triggers password reset to attacker-controlled inbox → full account takeover. Vertical IDOR (BFLA) can grant admin access directly. CVE-2025-27507 (ZITADEL, CVSS 9.0) demonstrates IDOR enabling full instance-level account takeover for all LDAP users.
No. UUID v4 (random) prevents enumeration-based IDOR but does not prevent access once a reference is legitimately obtained (shared link, API response to a third party, server logs). UUID v1 is partially predictable via timestamp sandwich attacks — CVE-2024-45719 (Apache Answer) shows exactly this. Authorization checks are mandatory regardless of identifier type.
UUID v1 is derived from a timestamp and MAC address. An attacker who knows the approximate account creation time and has any UUID from the application can compute or brute-force nearby UUIDs. UUID v4 is cryptographically random (122 bits of entropy) and is not enumerable. However, neither type is a substitute for server-side ownership checks.
Create two separate user accounts (A and B). Record resources owned by account B. From account A's session, attempt to access those resources by substituting B's identifiers in URL paths, query parameters, request bodies, and headers. Test all HTTP methods (GET, PUT, PATCH, DELETE). Confirm with body analysis: if A's request returns B's data, IDOR is confirmed. Burp Suite's Autorize extension automates this two-session replay.
1) Derive user identity exclusively from the authenticated session — never from request input. 2) Include the ownership constraint in every database query (WHERE owner_id = session_user_id). 3) Return 404 (not 403) on unauthorized access to prevent resource existence oracle. 4) Use UUID v4 for externally exposed identifiers. 5) Enforce PostgreSQL Row-Level Security as a database-layer backstop. 6) Test with cross-user assertions in CI/CD.
PostgreSQL RLS enforces ownership at the database engine level. A policy such as USING (owner_id = current_setting('app.user_id')::int) makes it structurally impossible to return another user's rows regardless of application code correctness. Even if a developer forgets the WHERE clause, the database rejects the query. RLS cannot be bypassed by application-layer bugs.
Horizontal privilege escalation occurs when a user accesses resources belonging to another user at the same privilege level. User A reads or modifies User B's orders, messages, or profile data. This is the most common IDOR variant — it accounts for the majority of high-severity bug bounty IDOR reports.
Vertical privilege escalation (BFLA — Broken Function Level Authorization) occurs when a regular user invokes endpoints or HTTP methods restricted to admins or higher-privileged roles. The attacker accesses admin dashboards, user management APIs, or audit logs without having the required role. CVE-2025-27507 is a real example: authenticated non-admin ZITADEL users calling Admin API endpoints.
In multi-tenant architectures, an IDOR crossing tenant boundaries exposes an entire organization's dataset rather than one user's records — blast radius is amplified by a factor of thousands. Common failure: WHERE clauses missing tenant_id, or tenant_id accepted as a user-controlled request parameter. CVSS scores for cross-tenant IDOR consistently reach 8.8–9.5.
Partially. DAST tools detect IDOR only with multi-session configuration (two authenticated identities). SAST tools (Semgrep AI, 2025) achieve 22% true positive rate on single-file flows but fail completely when authorization spans multiple files or middleware. Manual two-account testing remains the gold standard. BreachVex automates differential multi-session testing with semantic ownership analysis.
Average IDOR remediation cost is approximately $25,000 (bounty + engineering time). The Optus breach (IDOR, 2022) resulted in an AUD $1.36M regulatory fine. The McDonald's McHire IDOR (July 2025, 64 million records) is the largest known IDOR breach to date. Bug bounty payouts range from $3,000 (typical) to $12,500 (HackerOne report #2122671, GraphQL IDOR).
A typical horizontal IDOR (read-only PII) scores CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N = 6.5 High. A write-capable IDOR scores CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N = 8.1 High. CVE-2025-27507 (ZITADEL IDOR → full account takeover) scores 9.0 Critical.