Security Basics

9 min

How to Test for IDOR Vulnerabilities Safely

How to Test for IDOR Vulnerabilities Safely

Learn how to test for IDOR vulnerabilities in a live SaaS app, confirm broken object access safely, and apply fixes that protect every customer record.

Learn how to test for IDOR vulnerabilities in a live SaaS app, confirm broken object access safely, and apply fixes that protect every customer record.

A user changes /api/invoices/1842 to /api/invoices/1843 and receives another customer’s invoice. That is an insecure direct object reference, or IDOR, and it can turn an ordinary feature into a data exposure, account takeover, or fraud problem. Learning how to test for IDOR vulnerabilities is therefore not about guessing IDs at random. It is about proving that every object request is checked against the identity, role, and tenant of the person making it.

IDOR is also commonly called broken object-level authorisation, or BOLA. It occurs when an application accepts a direct reference to an object - such as a database ID, UUID, filename, order number, or user ID - but fails to verify whether the current user is allowed to access or alter that object.

For a startup, the risk is rarely theoretical. An AI-built application may correctly hide another customer’s record in the interface while leaving the underlying API endpoint wide open. The button is not the security control. The server-side authorisation check is.

Start with permission and a safe test setup

Only test applications, accounts, and environments you own or have explicit permission to assess. For a live product, use two controlled test accounts that belong to different users or organisations. Do not access real customer records merely to demonstrate a finding.

Create Account A and Account B with clearly different ownership boundaries. If your product is multi-tenant, place each account in a separate workspace or organisation. Seed each account with harmless test data: a project, invoice, support ticket, document, saved payment method, or profile. The aim is to establish an expected rule: Account A should never read, edit, delete, download, or trigger actions on Account B’s objects.

This two-account approach matters because an IDOR test is a comparison. You first capture a request that legitimately works for Account A, then repeat the relevant request as Account B while changing only the object reference. If Account B gets data or can perform an action, you have evidence of an authorisation failure.

Map every place your app exposes an object reference

Before changing requests, identify where your application handles direct references. IDs do not always appear as obvious sequential numbers. Modern applications often use UUIDs, opaque-looking tokens, slugs, filenames, or nested route parameters such as /organisations/acme/projects/7f3....

Review the browser’s network activity while moving through the application. Pay particular attention to API calls for dashboards, detail pages, exports, file downloads, comments, invitations, checkout sessions, admin tools, and background actions. A proxy tool such as Burp Suite can make it easier to inspect and replay requests, but browser developer tools are enough to begin.

Also inspect your repository. Search route handlers, server actions, API controllers, Supabase queries, and database functions for parameters named id, userId, organisationId, projectId, invoiceId, or fileKey. AI-generated code often wires a supplied ID directly into a query because the happy-path feature works. The missing question is whether that ID belongs to the authenticated caller.

For each endpoint, record three things: the object reference, the action being requested, and the expected permission rule. A GET /api/projects/:id request may require ownership or workspace membership. A DELETE /api/users/:id request may require both ownership and a recent authentication check. A payment refund endpoint should require a tightly controlled finance or administrator role, not simply any logged-in user.

Test reads, writes and high-impact actions separately

A common mistake is to test only whether another user can view a record. Read access is serious, but IDOR flaws can be more damaging when they allow changes.

Start with a request made by Account A, such as viewing one of its projects. Copy the object ID from the request, sign in as Account B, and make the equivalent request using Account A’s ID. A secure application should return a denial response, commonly 403 or 404, without revealing object details. Whether you use 403 or 404 depends on your product design. A 404 can reduce object enumeration, but either response must be consistent and must not leak useful metadata.

Then repeat the test for modification requests. Change Account A’s object reference in an update, archive, delete, invite, export, or download request while authenticated as Account B. Check the result after the request, not only the response body. Some endpoints return a vague success response even when no update occurred; others may queue work that completes later.

High-impact actions deserve extra attention. Test whether one user can create payment links for another merchant, cancel another account’s subscription, send invitations into a workspace they do not control, regenerate API keys, view audit logs, or retrieve signed file URLs. A single missing ownership check on these flows can create a route to fraud or wider account compromise.

Do not forget indirect references. An endpoint may safely check projectId but accept an unprotected memberId or attachmentId in the request body. Likewise, a bulk endpoint such as POST /api/projects/archive may verify the first ID in an array but fail to check each remaining item.

Check tenant boundaries, roles and session state

IDOR testing is not limited to two ordinary users. Your access model may include workspace owners, members, viewers, contractors, support agents, and internal administrators. Every combination creates a rule worth checking.

For a multi-tenant SaaS product, test cross-workspace access even if the user has a valid session in both workspaces. The server must derive the active tenant from trusted session context or verify membership against the requested object. Trusting an organisationId supplied by the browser is a frequent mistake.

Test role changes as well. A member who is downgraded from editor to viewer should lose write access immediately. A removed user should not retain access through an old JWT, cached route response, signed URL, or long-lived session. If you use impersonation or support tooling, test those features with particular care. They often bypass the normal permission path by design, which makes narrow scope and audit logging essential.

Test Supabase at both the API and database layers

Supabase applications need two aligned controls: application logic and row-level security, or RLS. A server route may look safe but query with a service-role key, bypassing RLS entirely. Conversely, the front end may query the database directly, making correct RLS policies the final enforcement point.

For tables containing customer-owned data, verify that RLS is enabled and that policies cover SELECT, INSERT, UPDATE, and DELETE as appropriate. Test with Account B’s authenticated JWT against rows owned by Account A. The query should return no rows and should not modify anything.

Be careful with policies that only check the new value on an update. An UPDATE policy needs to control both which existing rows a user may target and what the resulting row may contain. Without the correct checks, a user may be able to move a record into another organisation, change its owner, or update a row they should never have selected.

Never expose a Supabase service-role key in a client bundle, environment variable available to the browser, or public repository. Once that key is exposed, RLS is bypassed and an IDOR-style flaw becomes a database-wide exposure.

Fix the authorisation decision where it belongs

The durable fix is server-side, close to the data access or business action. Do not rely on hidden buttons, client-side route guards, unpredictable UUIDs, or the assumption that users will not discover an endpoint. Those measures may reduce casual misuse, but none decides permission.

For each request, derive the authenticated user from a verified session or JWT. Load the target object, then confirm that the user owns it, belongs to its organisation, or has the required role before returning data or performing the action. In database-backed systems, scope queries directly where possible: request the record by both its object ID and authorised tenant or owner ID, rather than fetching by ID and hoping a later conditional handles it correctly.

Centralise repeated checks in an authorisation helper or policy layer. This reduces the chance that a new route, generated quickly from a prompt, misses a critical condition. It also makes review faster: an auditor can trace a sensitive action to a consistent decision point rather than inspect a different hand-written check in every controller.

Use opaque IDs where it makes product sense, but treat them as a usability and enumeration control, not authorisation. A UUID that leaks in a URL, notification, API response, or log is still an object reference. The permission check remains mandatory.

Retest after the fix and preserve the evidence

After remediation, repeat the exact request that demonstrated the issue. Confirm that the unauthorised account cannot read, alter, delete, download, or invoke the relevant action. Then test the legitimate path to make sure the correction has not blocked valid access.

Add automated tests for the boundary you found. A useful test states that a user from Workspace B receives no project from Workspace A, cannot update it, and cannot access its attachments. Include similar tests when adding new endpoints, bulk operations, exports, and administrative features.

Record the affected route, object type, roles tested, result, severity, and exact fix. That creates a usable trail for engineering and gives founders a clear answer to the business question: could one customer touch another customer’s data?

IDOR defects are often small pieces of code with very large consequences. Treat every object ID arriving from a browser as untrusted, verify access at the point of use, and test the boundary before your users test it for you. If your team needs an independent check across a live app and repository, HollowByte can identify the affected paths and provide the specific remediation work needed to close them.

Back to blog