Resource library

Cyber QA

Testing for IDOR: A QA Guide (2026)

Learn testing for IDOR with actor-resource matrices, REST and GraphQL checks, runnable Node.js automation, authorization oracles, and interview answers.

25 min read | 3,565 words

TL;DR

Testing for IDOR requires controlled identities and known resources. Prove an owner can perform the action, keep that identity fixed while substituting another user's reference, and verify the server denies access everywhere the object can appear.

Key Takeaways

  • Test IDOR as an actor-resource-action-context policy, not as a predictable-number problem.
  • Use paired owner and nonowner fixtures so every negative result has a valid positive baseline.
  • Change one reference while keeping the same credential, business input, and resource state.
  • Verify reads, writes, lists, search, exports, files, GraphQL paths, caches, and asynchronous work.
  • Treat UUIDs and signed links as defense in depth, with explicit scope, expiry, and revocation rules.
  • Assert safe response behavior plus unchanged authoritative state, side effects, and audit records.

Testing for IDOR means proving that every object reference is authorized for the current actor, action, tenant, relationship, and resource state. A reliable QA test changes a reference while keeping the authenticated identity constant, then verifies that unauthorized data and side effects remain inaccessible through every alternate path.

IDOR is an access-control defect, not an identifier-format defect. Replacing 42 with a UUID can reduce guessing, but it does not authorize a user who obtains someone else's UUID. This guide shows how to build precise horizontal, vertical, and contextual authorization tests for web applications, APIs, GraphQL, files, exports, and asynchronous workflows.

TL;DR

Test question Expected secure behavior
Can user A read user B's object by changing an ID? No private fields or existence details beyond the documented concealment policy
Can user A update or delete user B's object? Denial before state change, job creation, or downstream effect
Does a UUID prevent IDOR? No, it only makes blind enumeration harder
Is hiding the UI control sufficient? No, the API and data-access layer must enforce authorization
What is the best test design? Paired allowed and denied actors against the same operation and controlled resource fixtures
What should automation assert? Status, safe response shape, authoritative state, side effects, caches, events, and audit decision

1. Understand the Testing for IDOR Security Boundary

An insecure direct object reference occurs when client-controlled data identifies an object and the server fails to verify that the current principal may perform the requested action on that object. The reference may be a database ID, UUID, filename, document key, order number, slug, cursor, storage path, composite key, or opaque token.

The essential model has four parts:

  • Actor: authenticated user, service, guest, administrator, or delegated principal.
  • Resource: invoice, ticket, profile, file, message, device, tenant, or child object.
  • Action: read, list, update, delete, approve, download, share, or transition.
  • Context: tenant, ownership, relationship, role, time, resource state, delegation, or policy version.

Authentication answers who the caller is. Object-level authorization answers whether that caller can perform this action on this particular resource now. A route protected by login can still expose every customer's records. Client-side ownership checks, disabled buttons, and hidden URLs do not change the server's obligation.

IDOR is often horizontal, such as one standard user reading another standard user's invoice. It can also be vertical, such as a standard user referencing an administrator-only object, or contextual, such as an approver acting on a request from the wrong department. Keep these dimensions visible in coverage instead of reducing every case to owner versus nonowner.

The HTTP 401 vs 403 guide explains authentication, permission denial, and deliberate 404 concealment. For IDOR, consistent non-disclosure matters, but the exact public status is secondary to preventing access.

2. Inventory Every Direct and Indirect Object Reference

Capture normal traffic for each role and mark every value that selects an object. Inspect URL paths, query strings, JSON and form bodies, GraphQL variables, headers, cookies, multipart fields, WebSocket messages, hidden form values, mobile API traffic, and generated links. Do not stop at obvious id parameters. Names such as account, owner, documentKey, parent, cursor, and returnTo may influence object lookup.

Build an inventory with operation, reference location, object type, allowed actor rule, expected denial, and state oracle. Include collection and search operations because an application can protect GET /orders/{id} while leaking the same order through /orders?customerId=..., autocomplete, export, activity feeds, or counts.

Indirect references still need authorization. A signed download URL may be copied to another user. A share token may grant intentionally anonymous access. A cursor may encode an object position. For each, document whether possession is the authorization mechanism, how it is generated, its scope, expiry, revocation, and whether it leaks into logs or referrers.

Trace parent-child relationships. If a route is /projects/{projectId}/tasks/{taskId}, test whether the task actually belongs to the supplied project and whether the actor can access both. Some data layers load only taskId and ignore the parent, which enables cross-project access. Reverse the case too: a valid project must not make every child ID available.

Inventory alternate representations: HTML page, JSON API, PDF, CSV, thumbnail, original upload, cached preview, event payload, notification, and audit history. One authorization decision should protect the resource across all paths, but tests must prove each implementation calls it.

3. Create Identities and Resources That Expose Policy Gaps

The strongest IDOR fixture is a pair, not a random number. Create user A and user B with the same least-privileged role. Give each a uniquely labeled resource. A must access A's object successfully and fail to access B's object through the same operation. This paired result separates object authorization from general authentication or route availability.

Add a third identity when vertical or tenant rules matter. A practical fixture set is:

  • tenant_a_owner, who owns resource A.
  • tenant_a_peer, with the same role but no relationship to resource A.
  • tenant_a_manager, allowed to read or approve resource A according to policy.
  • tenant_b_owner, who owns a similarly shaped resource in another tenant.
  • global_support, with narrowly documented support access.
  • anonymous, with no credential.

Verify the effective identity before the security action. Query a safe /me endpoint or inspect a signed test fixture through trusted setup. Shared accounts and mislabeled tokens cause false results. Never infer identity from the username used during setup if token exchange or impersonation follows.

Use known IDs rather than brute forcing. Authorization testing does not require high-volume enumeration. Seed deterministic objects through an approved setup API, record their references, and test exact allowed and denied pairs. This is safer, faster, and produces clearer evidence.

State also belongs in fixtures. Create draft, submitted, approved, archived, and deleted objects if policy changes by lifecycle. Add shared, unshared, delegated, and expired relationships. The test name should state the rule, such as peer cannot download unshared report, not user2 gets 403.

4. Compare Horizontal, Vertical, Contextual, and Relationship Tests

Different access-control defects need different actor-resource pairs. A matrix prevents an overfocus on sequential IDs.

Access pattern Example Pair to test Main oracle
Horizontal Customer A reads customer B's invoice Same role, different owners No data plus unchanged access record
Cross-tenant Tenant A user reads tenant B project Same role, different tenants Concealed or forbidden response, no cache leak
Vertical Standard user invokes admin export Lower and higher privilege Function denied before job creation
Relationship Nonmember reads private team document Member and nonmember Membership policy enforced on every path
State-dependent Author edits an approved article Same actor, different states Transition policy and unchanged approved content
Field-level Support agent reads payment token Same object, different field authority Sensitive field omitted or denied by contract
Delegated Assistant acts after delegation expiry Active and expired grant Expired relationship cannot authorize
Possession-based Anyone with share link reads file Valid, expired, revoked, and altered token Exact scope, expiry, and revocation enforced

For each row, test read and write independently. A service may correctly hide another user's record on GET but accept its ID in PATCH or DELETE. Also test creation. Mass assignment of ownerId or tenantId can create an object inside another security boundary even when later reads are protected.

Do not expect every denied response to be identical internally. Public behavior may deliberately conceal resource existence with 404, while internal audit data records a forbidden decision. Test both channels using authorized observability. A random nonexistent ID and a real forbidden ID should not expose a useful difference through body fields, headers, filenames, cache keys, or gross timing.

5. Perform a Precise Manual IDOR Test

Begin with a successful owner request. Capture method, URL, reference values, authentication, body, response, and resulting state. Replay it unchanged to confirm the fixture is stable. Then keep user A's credential and substitute only user B's known object reference. A secure application denies access or conceals the object according to policy.

For reads, inspect the complete response, not only status. Look for names, totals, nested objects, error metadata, ETags, Content-Disposition filenames, redirect locations, presigned URLs, and cached fragments. A 403 response containing the invoice amount is still a disclosure. A 302 redirect can leak an object key in its destination.

For writes, query state through a trusted owner or administrative oracle after the denied request. Verify version, timestamps, child records, events, messages, emails, exports, storage, and audit-success entries. The controller may return denial after a downstream write. For deletes, verify the object remains usable, not merely present in soft-delete storage.

Change references in every location independently. If the path contains object A but the body contains object B, the server must reject ambiguity rather than authorizing one and mutating the other. Test duplicate query parameters and composite keys. Use valid business input so schema validation does not block the request before authorization.

Then test nearby paths: list, search, count, batch, export, preview, share, history, attachment, mobile version, and legacy version. Reuse one controlled object and identities. Stop if an unexpected mutation occurs, restore the fixture, and report before expanding scope.

The API security testing with OWASP guide provides a broader workflow for organizing authorized security checks and evidence.

6. Automate Testing for IDOR with Node.js

The following Node.js test uses built-in node:test and fetch, so it needs no test framework dependency on a current Node release. It runs only when six environment variables are present. Point it at an authorized test environment with two disposable users and invoices. Run node --test idor.test.mjs.

import assert from "node:assert/strict";
import { test } from "node:test";

const config = {
  baseUrl: process.env.BASE_URL,
  tokenA: process.env.USER_A_TOKEN,
  tokenB: process.env.USER_B_TOKEN,
  invoiceA: process.env.USER_A_INVOICE_ID,
  invoiceB: process.env.USER_B_INVOICE_ID,
  expectedDenial: Number(process.env.EXPECTED_DENIAL ?? 404)
};

const configured = Object.values(config).every((value) => value !== undefined);

async function getInvoice(token, invoiceId) {
  return fetch(`${config.baseUrl}/api/invoices/${encodeURIComponent(invoiceId)}`, {
    headers: { authorization: `Bearer ${token}` },
    redirect: "manual"
  });
}

test("each owner can read the matching invoice", { skip: !configured }, async () => {
  const [responseA, responseB] = await Promise.all([
    getInvoice(config.tokenA, config.invoiceA),
    getInvoice(config.tokenB, config.invoiceB)
  ]);

  assert.equal(responseA.status, 200);
  assert.equal(responseB.status, 200);

  const [invoiceA, invoiceB] = await Promise.all([
    responseA.json(),
    responseB.json()
  ]);
  assert.equal(String(invoiceA.id), config.invoiceA);
  assert.equal(String(invoiceB.id), config.invoiceB);
});

test("user A cannot read user B's invoice", { skip: !configured }, async () => {
  const response = await getInvoice(config.tokenA, config.invoiceB);
  const body = await response.text();

  assert.equal(response.status, config.expectedDenial);
  assert.doesNotMatch(body, new RegExp(config.invoiceB.replace(/[.*+?^${}()|[\]\\]/g, "\\
amp;"))); assert.equal(response.headers.get("location"), null); }); test("user B cannot read user A's invoice", { skip: !configured }, async () => { const response = await getInvoice(config.tokenB, config.invoiceA); assert.equal(response.status, config.expectedDenial); });

The baseline owner tests are essential. If both owner calls fail, the negative result says nothing about IDOR. Extend this pattern with PATCH and DELETE, then read the resource as its owner to prove unchanged state. Keep tokens out of command history and CI logs by using a secret store.

For a data-driven suite, store policy cases as actor, action, resource, expected decision, and oracle. Generate tests only from approved fixtures. Do not generate numeric ranges and call it coverage. Known relationships give far more diagnostic value than blind volume.

7. Test REST Paths, Bodies, Lists, and Bulk Operations

REST-like APIs expose references in more places than item routes. Test path IDs, filters, sort keys, nested resources, versioned URLs, alternate content types, JSON Patch paths, form fields, and method overrides. A query such as GET /orders?ownerId=B needs the same scoping as GET /orders/B even if the list response contains only summaries.

On create and update, attempt to supply server-owned fields such as ownerId, tenantId, createdBy, role, accountId, or status. The correct behavior may be rejection or safe ignoring, but it must not cross a boundary. Fetch the resulting object through a trusted oracle to verify actual ownership. Response echoing is not authoritative.

Nested routes need parent consistency. Test valid parent A plus child B, valid child A plus parent B, a parent from another tenant, and a child whose relationship changed. Database queries should scope through authorized relationships, not load a global child by ID after checking only the parent URL.

Bulk endpoints are especially risky. Send a list containing allowed A1, forbidden B1, and nonexistent X. The product must define atomic or partial behavior, per-item errors, and disclosure. Verify no forbidden item is returned or modified. If partial success is allowed, ensure indexes and error details do not reveal B1's private properties.

Pagination can leak objects through cursors, totals, and boundary shifts. Use stable fixtures and verify a user's pages never include another tenant's records. Changing a cursor should not bypass the original filter or authorization context. Export and report endpoints must reapply access at job execution time, not trust a list of IDs collected by the client.

8. Test GraphQL Object-Level Authorization

GraphQL commonly exposes object references through arguments, global node IDs, input objects, nested relationships, aliases, fragments, and mutation payloads. One HTTP endpoint does not mean one authorization decision. Resolver and data-loader paths must enforce access for each returned object and protected field.

Start with a normal operation for resource A, then substitute B's known ID while keeping A's token. Test the root field, node(id:) if present, search, connections, nested relationships, and mutation input. An application may protect invoice(id:) but return the same invoice through customer(id:) { invoices } or a global node interface.

Request minimum fields first, then sensitive fields. A resolver might allow the object shell but require additional authority for payment details, email, internal notes, or audit history. Assert both data and errors. GraphQL can return HTTP 200 with partial data and field errors. A status-only test will miss disclosure and denial.

Aliases and batching can expose inconsistent caching or authorization. Query the same object through two paths in one operation and use mixed allowed and forbidden IDs. Test server-supported HTTP batching separately because it is not the same as aliases. Confirm data loaders include tenant and permission context in cache keys and never share object results across requests or users.

Mutations must authorize the target resolved on the server. Supplying ownerId in an input must not transfer ownership unless that action is explicitly allowed. After a denied mutation, query through the true owner and inspect events. See the GraphQL API testing guide for envelope, variable, nullability, and schema coverage that complements IDOR tests.

9. Cover Files, Storage, Exports, Search, and Cached Representations

Files often bypass application controllers. Test original uploads, thumbnails, transformed images, PDFs, exports, backups, and attachments at every URL variant. Change path segments, object keys, filenames, and query references using known fixtures. Inspect redirects and signed URLs without following them automatically, because the first response can disclose a usable storage location.

For presigned or share URLs, determine whether possession intentionally grants access. Then test scope, method, content disposition, expiry, revocation, one-time behavior if promised, and whether permission removal invalidates existing links. Long random tokens reduce guessing, but leaks through logs, analytics, referrers, support tickets, or browser history can still transfer authority.

Search and autocomplete can leak existence, labels, snippets, avatars, counts, or sort position. Query exact private identifiers and distinctive fixture text as an unauthorized user. Test empty query, broad filters, typo correction, advanced syntax, and export of search results. The authorization filter must be applied before aggregation and highlighting.

Caches need identity-aware keys and safe directives. Test user A's successful response, then request the same URL as B and anonymous through the deployed edge. Reverse the order with a denial first. Check CDN, server, GraphQL data loader, browser service worker, and client query cache boundaries. A correct origin authorization decision cannot repair a shared cache that serves A's body to B.

Notifications and activity feeds are alternate representations too. A denied user must not receive an object title in a push message, email preview, webhook, or real-time event. Remove access and test existing subscriptions and open browser tabs.

10. Test Asynchronous Jobs, Race Conditions, and Permission Changes

An endpoint can authorize correctly at submission and fail later when a worker processes attacker-controlled IDs. Export, import, report, media, billing, and approval jobs should carry a trusted principal and re-evaluate the necessary policy at execution. Test a job containing allowed and forbidden objects, and inspect its output directly through a trusted fixture.

Permission changes create time boundaries. Start with access allowed, remove membership or sharing, then repeat item, list, cached, file, and real-time access. Document expected propagation through tokens, policy caches, replicas, search indexes, signed URLs, and open connections. A stale window may be an accepted design choice, but it must be explicit and proportionate to risk.

Race tests should target realistic state transitions, not flood the service. Coordinate two requests in a lab: one revokes access while another updates or exports the resource. Verify the final state follows a defined serialization or authorization rule. A time-of-check to time-of-use gap can allow work after the permission was removed.

Soft-deleted and archived objects need policy too. Test direct IDs, restore endpoints, versions, trash, and backups. A user who once owned a record may not retain access after transfer. A support agent's temporary impersonation must end across caches and jobs when the session closes.

Audit events should record actor, normalized resource reference, action, decision, policy version, and correlation ID without copying sensitive content. Verify denied requests create appropriate security evidence but never a business-success event.

11. Turn Findings into Durable Authorization Regression

A high-quality IDOR defect identifies the broken actor-resource-action rule. Include the allowed baseline, denied pair, modified reference location, observed private data or side effect, and affected alternate paths. Use disposable IDs and redact credentials. Do not title the report only IDOR in endpoint; state the outcome, such as Tenant member can download another tenant's invoice by changing fileId.

Root-cause remediation should centralize and scope data access. Prefer queries that begin from the current actor's authorized collection or tenant, then locate the requested object. Apply explicit policy checks for relationship and state. Random identifiers remain defense in depth, not the main fix. Client hiding and input validation cannot replace authorization.

Retest the original path, the opposite direction, create/update/delete variants, lists, exports, files, caches, and GraphQL resolvers that reach the same object. Add a policy regression case close to the authorization layer plus an end-to-end test through the deployed gateway. Verify a legitimate owner and authorized delegate still work.

Track coverage by policy rule and object type, not raw endpoint count. A useful dashboard shows which actor-resource-action combinations have positive and negative tests, which alternate representations exist, and which rules changed. Require a new case when a route introduces a client-controlled reference.

The durable goal is not to catch one predictable ID. It is to make unauthorized object retrieval structurally difficult and immediately visible whenever the data path changes.

Interview Questions and Answers

Q: What is IDOR?

IDOR is an object-level authorization flaw. The application accepts a client-controlled reference and accesses or modifies the object without confirming that the current actor may perform that action. The identifier can be numeric, random, encoded, or indirect.

Q: How do you test for horizontal IDOR?

I create two users with the same role and one resource each. I prove each owner can access the matching resource, then keep user A's credential and replace only the resource reference with B's known ID. I assert safe denial and verify state through B or a trusted oracle.

Q: Do UUIDs prevent IDOR?

No. UUIDs reduce blind guessing and can limit enumeration, but any leaked or shared UUID can still be used if authorization is missing. Server-side object-level permission checks are the primary control.

Q: Should an unauthorized object return 403 or 404?

Either can be correct according to the product's concealment policy. I test consistency and absence of private data, headers, cache leaks, and side effects. Internally, the audit trail should retain the true decision safely.

Q: How do you test IDOR in GraphQL?

I vary IDs in root fields, global node lookups, nested relationships, connections, and mutation inputs. I assert the GraphQL data and errors envelope, including partial data. I also test aliases, loaders, and caches for cross-user reuse.

Q: What is the difference between IDOR and mass assignment?

IDOR selects an object without adequate authorization. Mass assignment lets client input set fields that should be server-controlled, such as owner or role. They can combine when an update chooses another object and changes protected properties.

Q: How do you verify a denied write?

I query the authoritative state through the true owner or an administrative test oracle. I check versions, children, queues, events, emails, storage, and audit-success records. A denial response alone is not proof that no work occurred.

Q: What belongs in an IDOR regression suite?

Every sensitive actor-resource-action rule needs an allowed and a nearby denied case. I include alternate item, list, search, export, file, batch, GraphQL, and cache paths. Tests use deterministic fixtures instead of enumeration.

Common Mistakes

  • Treating an unguessable ID as authorization.
  • Testing only whether the route requires login.
  • Changing both the token and object ID, which hides the failing control.
  • Using random IDs without a positive owner baseline.
  • Checking status while ignoring leaked response fields, headers, redirects, and filenames.
  • Verifying a denied write without checking authoritative state and downstream effects.
  • Covering item GET but missing create, update, delete, list, search, export, batch, file, and event paths.
  • Forgetting parent-child consistency and cross-tenant relationships.
  • Trusting hidden buttons or client-side route guards.
  • Sharing data-loader, CDN, or application cache entries across principals.
  • Running high-volume enumeration when known controlled pairs would prove the rule safely.

Conclusion

Testing for IDOR is best approached as policy verification. Model the actor, resource, action, and context, create controlled allowed and denied pairs, alter one reference at a time, and verify both public response and authoritative state across every representation.

Start with two same-role users and one private object each. Automate the owner baseline and the cross-owner denial, then extend the exact rule to writes, lists, GraphQL, files, exports, caches, and asynchronous work. That small paired design becomes a durable object-level authorization safety net.

Interview Questions and Answers

What is IDOR?

IDOR is an object-level authorization defect. The server accepts a client-controlled object reference but does not verify the current actor's permission for that object and action. Identifier complexity is not the authorization control.

How do you design a horizontal IDOR test?

I create two same-role users with a private resource each. I prove both owner paths work, then keep user A's credential and use B's known resource ID. I assert safe denial and unchanged state.

What is the actor-resource-action model?

It describes who is acting, which object is targeted, and which operation is requested. I add context such as tenant, relationship, resource state, and delegation. Each meaningful policy rule gets an allowed and denied test.

Why do UUIDs not solve IDOR?

A UUID only reduces guessing. References can leak through APIs, logs, browser history, messages, and shared links. The server must still authorize every access.

How should APIs conceal forbidden resources?

The product may use 404 to conceal existence or 403 to express refusal. I test consistent status, body, headers, redirects, caching, and gross timing while internal audits preserve the real policy decision safely.

How do you test IDOR in nested REST routes?

I mix valid parents and children from different owners and tenants. The server must verify both actor access and the parent-child relationship, not load a child globally after checking only the parent path.

How do you test asynchronous IDOR paths?

I submit controlled exports or jobs with allowed and forbidden IDs, then inspect worker output and events. The job must carry trusted identity context and reapply authorization at the correct execution boundary.

What makes an IDOR regression suite durable?

Coverage is organized by policy rule and object representation rather than sequential IDs. It includes paired positive and negative cases across item, list, search, export, file, batch, GraphQL, cache, and event paths.

Frequently Asked Questions

What does IDOR mean in API testing?

IDOR is an object-level authorization failure where a client-controlled reference selects data or an action without a sufficient permission check. The reference may be a path ID, UUID, filename, body field, GraphQL variable, or token.

How do you test for IDOR without brute force?

Create two authorized test users and one known resource for each. Verify owner access, then use the same credential with the other known reference. This paired method is safer and more diagnostic than enumerating ranges.

Do UUIDs prevent IDOR vulnerabilities?

No. UUIDs make blind guessing harder but do not grant or verify permission. A leaked, shared, logged, or otherwise discovered UUID remains exploitable if object-level authorization is missing.

Should IDOR return 403 or 404?

Both can be valid depending on the concealment policy. The response must not leak private data or useful existence signals, and denied writes must leave state unchanged.

How do you test IDOR in GraphQL?

Vary object IDs in root fields, node lookups, nested relationships, connections, and mutation inputs. Assert both `data` and `errors`, then test aliases, loaders, batching, and caches for cross-user reuse.

What is horizontal privilege escalation?

It occurs when a principal accesses another principal's resources at the same nominal role level. Two standard customers accessing each other's invoices is a typical horizontal authorization defect.

How do you verify an IDOR write was denied?

Read the object through its true owner or a trusted administrative oracle. Check version, children, jobs, events, emails, files, and success audit entries rather than trusting the denial response alone.

Related Guides