Choose your stack
Fix missing object-level authorization (IDOR) in Node.js and Express
Take the user from your authentication middleware (req.user), never from the request, and put the same scope in every invoice query: tenant_id = $tenant AND (owner_id = $user OR $isBillingAdmin).
Open the fix card Python · FlaskFix missing object-level authorization (IDOR) in Python and Flask
Load the signed-in user from Flask's signed session in a before_request hook, never from request data, and add the same scope to every invoice query: tenant_id = :tenant AND (owner_id = :user OR :is_billing_admin).
Open the fix card Java · Spring BootFix missing object-level authorization (IDOR) in Java and Spring Boot
Get the user from Spring Security with @AuthenticationPrincipal, never from request parameters, and scope every repository query with tenant_id = :tenant AND (owner_id = :user OR :isBillingAdmin).
Open the fix card C# · ASP.NET CoreFix missing object-level authorization (IDOR) in C# and ASP.NET Core
Read the user's ID, tenant and role from the ClaimsPrincipal your authentication scheme builds, never from the request, and scope every invoice query with tenant_id = $tenant AND (owner_id = $user OR $isBillingAdmin).
Open the fix card TypeScript · Next.jsFix missing object-level authorization (IDOR) in TypeScript and Next.js
Get the user from your authentication inside each route handler and server action, never from the request, and put one scope in every invoice query: tenant_id = $tenant AND (owner_id = $user OR $isBillingAdmin).
Open the fix card Python · FastAPIFix IDOR and missing authorization in Python and FastAPI
Build the signed-in user in a dependency (current_user) from the authenticated session and the users table, never from request data, and put the same scope in every query's WHERE clause: same tenant, and the caller's own rows unless they are a billing admin.
Open the fix card Python · DjangoFix IDOR and missing authorization in Python and Django
Build one queryset that holds the rule, from request.user and its tenant and role, never from request data: Invoice.objects.filter(tenant=member.tenant), narrowed to owner=user for members.
Open the fix card Go · net/httpFix IDOR and missing authorization in Go and net/http
Build the signed-in user in middleware from the verified session and the users table, never from request data, and put the same scope in every query's WHERE clause: tenant_id = :tenant AND (owner_id = :user OR :is_billing_admin).
Open the fix card Ruby · RailsFix IDOR and missing authorization in Ruby on Rails
Load records through a scope built from the signed-in user, never by ID alone: visible_invoices(current_user).find(params[:id]), where the scope applies the tenant to everyone and the owner to members.
Open the fix card PHP · LaravelFix missing object-level authorization (IDOR) in PHP and Laravel
Build one query that holds the rule, from $request->user()'s tenant and role, never from request data: Invoice::where('tenant_id', $user->tenant_id), narrowed to owner_id for members.
Open the fix cardHow it happens
- Authentication is mistaken for authorization. The route confirms that someone is signed in, then runs SELECT ... WHERE id = ? with the ID from the URL, so every signed-in user can read every invoice by changing one number (CWE-639).
- The check exists on one route but not on the others. A detail page gets fixed after a pentest while the list, search, export and download endpoints keep their own queries; a bulk export that takes a list of IDs often checks none of them (CWE-862).
- The rule is incomplete. An admin shortcut such as owner OR role = 'billing_admin' never compares tenants, so a billing admin in one customer's account reads every other customer's invoices; sameTenant AND owner OR admin without parentheses does the same, because AND binds tighter than OR (CWE-863). Tests written inside a single tenant all pass.
- The identity comes from the request. A user ID or tenant ID in the body, the query string or an X-Tenant-ID header lets the caller answer the permission question themselves.
Fix principles
- Take the user ID, tenant and role from the authenticated session or validated token on the server. Treat IDs in the request only as selectors.
- Write the rule once and apply it in the query: tenant_id = :tenant AND (owner_id = :user OR :isBillingAdmin). Keep the tenant test outside the parentheses so it binds administrators too.
- Apply the same scope to every path that reads the object: detail, list, search, counts, export, download and background jobs. Scoping in SQL also keeps pagination and counts correct, which filtering after the query cannot.
- Answer 404 for objects outside the caller's scope, exactly as for missing ones, so IDs cannot be probed.
- Give bulk operations an explicit contract. These cards refuse the whole export when any requested invoice is missing or out of scope, so nobody mistakes a partial file for a complete one.
- If you also use the framework's per-object hooks (Spring's @PostAuthorize, ASP.NET Core's IAuthorizationService), compare the tenant before any role shortcut, and keep scoped queries for collections.
- Random IDs such as UUIDs make guessing harder, but links leak through logs, email and browser history; they do not replace the check.
The scenario
Invoices belong to an owner inside a tenant. Members read their own invoices; billing admins read every invoice in their own tenant and nothing outside it. The fixture has two tenants, red (Alice and Ben are members, Cara is the billing admin) and blue (Erin is a member, Dan is the billing admin). Each card exposes GET /invoices, GET /invoices/{id} and GET /invoices/export?ids=..., which returns CSV and refuses the whole export when any requested invoice is outside the caller's scope. The signed-in user comes from authentication (a test stub backed by the users table), never from the request body or query string.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
- The exploit tests sign in as a colleague in the same tenant, as a member of another tenant and as a billing admin of another tenant, and replay invoice IDs against the detail, list and export routes; each must get a 404 or exactly the in-scope rows. The functional tests keep the owner and a same-tenant billing admin working.
- Do not count on static analysis. For the stacks here the only nearby Semgrep and CodeQL rules are C# heuristics for MVC controllers (a missing [Authorize], or edit and delete actions), which minimal APIs and read endpoints never trigger; Semgrep's one IDOR rule, check-unscoped-find, is for Rails. Semgrep's OWASP Top 10 pack reported nothing on any vulnerable implementation. Each card's scannerRules lists what was checked.
- On a running system, repeat the same matrix with two accounts in each of two tenants: replay one account's object IDs with the other account's session on every route that takes an ID.
Identifiers
CWE-639 (authorization bypass through a user-controlled key) is the IDOR entry and describes the vulnerable detail and export routes; CWE-862 (missing authorization) covers the list that never applies the owner rule; CWE-863 (incorrect authorization) describes the wrong implementation, whose check exists but ignores the tenant. All three map to OWASP Top 10:2025 A01, and the 2025 CWE Top 25 ranks CWE-862 4th, CWE-863 17th and CWE-639 24th. CAPEC is left empty on purpose: MITRE lists no attack pattern for CWE-639 or CWE-863, and CWE-862 lists only CAPEC-665 (Thunderbolt), which does not fit.
References
- OWASP API Security Top 10 2023: API1 Broken Object Level Authorization
- OWASP Insecure Direct Object Reference Prevention Cheat Sheet
- OWASP Authorization Cheat Sheet
- OWASP Multi-Tenant Security Cheat Sheet
- OWASP Top 10:2025 A01 Broken Access Control
- CWE-639: Authorization Bypass Through User-Controlled Key
- CWE-862: Missing Authorization
- CWE-863: Incorrect Authorization
Practice in the browser
- Excessive agency lab: Bind an agent’s action to the approved arguments · Advanced, JavaScript
- IDOR lab: Repair an invoice authorization bug · Foundation, JavaScript
- IAM trust policy lab: Evaluate a role trust policy against the confused deputy · Practitioner, Python
Guides on this weakness
Enforce authorization for every object action
Authorize the authenticated subject, requested operation, and specific resource together; a valid login or unpredictable identifier is insufficient.
Review tenant boundaries before implementation
Trace tenant identity through requests, data stores, caches, jobs, and administration, then turn plausible isolation failures into tests.
Contain prompt injection at the application boundary
Assume retrieved content can influence model output; enforce data access and consequential actions in components the model cannot authorize for itself.
Review trust boundaries in mobile and native clients
Protect local data and platform interactions while keeping server-side authorization independent of the client’s behavior.
Authorize functions and fields as well as objects
Prevent mass assignment, sensitive-field exposure and administrative actions that a valid object lookup alone cannot protect.
Review a code change through its trust boundaries
Turn a pull request into a small set of security claims that can be checked against the surrounding system and tests.
Review cloud permissions and role trust together
Distinguish who can obtain a cloud identity from what that identity can do, then test the effective access path.
Authenticate workloads and authorize their service actions
Use service identities, mutual TLS and function permissions without mistaking an encrypted connection for complete authorization.
Workload identity federation: short-lived, scoped identities for services and CI
Trace how a running workload obtains a short-lived identity, write cloud trust policies that match immutable claims instead of names or wildcards, and keep the resulting credential limited by audience and authorization.
Authorization models: RBAC, ABAC, ReBAC and policy as code you can test
Translate roles, attributes and relationships into explicit subject-action-resource decisions, prevent broken object level authorization (BOLA), and verify ownership and tenant boundaries with tests.
Keep retrieval inside the user’s data boundary
Carry document permissions through ingestion, retrieval, caching and citations so a model never receives another tenant’s records.
Authorize agent tools outside the model
Bind each proposed action to the user, resource and approved arguments before allowing a tool to cause a side effect.
Constrain MCP and multi-agent trust boundaries
Validate each connection and delegated action without treating a token for another service, a tool description, another agent or a state handle as authority.
SCIM deprovisioning: make account disablement remove every usable authority
Connect SCIM resource updates to current sessions, tokens, group-derived permissions and background jobs, with explicit tenant mapping, CAEP revocation events and a measurable revocation contract.
Abuse case library: tests and controls by feature
Recurring abuse cases for sign-up and login, password reset, MFA enrolment, invites, file upload, payments and coupons, webhooks, search, export, admin and AI chat or agents: the attacker goal, preconditions, a concrete test and the control, each linked to CWE and CAPEC.
Threat pattern library for threat modeling
Thirty-two recurring threat patterns grouped by identity, authorization, parsers, concurrency, integrations, data exposure, supply chain, availability and AI agents, each with where it shows up on a data-flow diagram, mitigations, a verification step and STRIDE, CWE and CAPEC links, plus a YAML catalog and a script that lists patterns per diagram element.
Secure design patterns: a catalogue for insecure-design (A06)
A catalogue of reusable secure-design patterns, grouped by concern, each with when to use it, a short example and the failure it prevents: choke-point authorization, capability tokens, parse-don't-validate, safe-by-default APIs, tenant scoping, fail-closed, idempotency, and more.
Worked threat model: a mobile banking app
A complete worked threat model for a mobile banking-style app: the data-flow diagram and its elements, STRIDE threats on the client, the transport and the backend, and the mitigations mapped to OWASP MASVS v2 controls and current Android and iOS platform APIs.
Worked threat model: an LLM agent with tools
A complete worked threat model for an LLM agent with tools: the data-flow diagram, the threats from prompt injection, excessive agency, data exfiltration, confused-deputy and memory poisoning, and the mitigations, mapped to the OWASP Top 10 for LLM Applications 2026 and the OWASP Top 10 for Agentic Applications 2026.
Case study: First American's EaglePro document exposure (2019)
How sequential IDs in unauthenticated links exposed First American's title documents in 2019, after its own penetration test found the flaw and a clerical error let it go unfixed.