Cyber QA
Testing broken access control: A QA Guide (2026)
Learn testing broken access control with role matrices, tenant isolation, API automation, negative cases, and interview-ready QA methods for 2026 teams.
22 min read | 3,549 words
TL;DR
Testing broken access control means proving that each identity can perform only the allowed action on the allowed resource, in the allowed tenant and state. Model the policy, send requests directly to every server-side boundary, change one authorization dimension at a time, and verify that denial causes no data exposure or state change.
Key Takeaways
- Build an authorization matrix from subjects, resources, actions, ownership, tenant, and lifecycle state before writing cases.
- Verify every protected action at the server boundary because hidden controls and client-side route guards are not authorization.
- Test horizontal, vertical, object-level, property-level, and function-level access with controlled identities and known data.
- Assert denied side effects and data non-disclosure, not only a 401 or 403 status code.
- Cover alternate methods, bulk operations, exports, files, background jobs, caches, and indirect identifiers.
- Automate a compact deny matrix in CI while keeping exploratory abuse cases for newly changed trust boundaries.
Testing broken access control is the disciplined process of proving that users, services, and anonymous clients cannot read or change resources outside their authorization policy. A strong QA approach goes beyond checking whether an Admin button is hidden. It exercises the server directly, crosses role and tenant boundaries, manipulates identifiers, and verifies that a rejected operation produces no protected side effect.
This guide treats authorization as a testable business rule. You will build a coverage model, design high-signal negative cases, automate an authorization matrix with runnable Node.js tests, and explain the subject clearly in a QA or SDET interview. Use the techniques only against systems you own or are explicitly authorized to test.
TL;DR
| Question | Practical answer |
|---|---|
| Where should testing start? | At the server-side policy and resource boundary, not the visible UI |
| What identities are required? | Anonymous, ordinary users, relevant roles, resource owners, non-owners, and users from another tenant |
| What must a denial prove? | No sensitive response, no mutation, no event, no file, and no indirect disclosure |
| Which cases find the most gaps? | Owner versus non-owner, lower role versus privileged action, cross-tenant IDs, alternate methods, and bulk or export paths |
| What belongs in CI? | A data-driven allow and deny matrix for stable resources plus checks for unchanged state |
The core oracle is simple: authorization must be evaluated for the current subject, requested action, target resource, tenant, and relevant state on every request. Authentication tells the system who the caller claims to be. Authorization decides what that caller may do. HTTP 401 versus 403 behavior helps with protocol semantics, but the security result matters more than forcing every application into one response style.
1. Define Broken Access Control as a Testable Policy
Broken access control exists when an application fails to enforce an intended restriction on a protected resource or operation. The failure can expose a record, permit an unauthorized mutation, reveal a field, start a privileged job, download a file, or invoke an administrative function. It can occur even when login, token validation, and encryption all work correctly.
Translate the product policy into six dimensions:
- Subject: anonymous client, user, support agent, administrator, service account, or API client.
- Resource: account, order, document, report, secret, job, file, or configuration.
- Action: create, list, read, update, delete, approve, export, share, or execute.
- Relationship: owner, collaborator, manager, unrelated user, or delegated actor.
- Scope: tenant, organization, project, region, or environment.
- State: draft, submitted, approved, archived, locked, or deleted.
A requirement such as 'managers can approve expenses' is incomplete. Ask whether a manager can approve their own expense, an expense in another department, an already rejected expense, or an amount above a delegated limit. Ask whether the API, mobile client, batch endpoint, and background retry all apply the same policy. These questions turn an attractive permission label into observable rules.
Do not define success only as receiving 403. Some systems intentionally return 404 for inaccessible objects to reduce resource enumeration. Some unauthenticated clients receive 401, while an authenticated but unauthorized client receives 403. Record the expected external contract, then separately verify the invariant: the caller learned nothing protected and changed nothing protected.
2. Build the Authorization Matrix Before Testing Broken Access Control
An authorization matrix exposes missing combinations before execution. Put resources on rows and actions on columns, then add policy context for roles, ownership, tenant, and state. A useful slice might look like this:
| Subject and context | Read invoice | Edit draft | Approve | Export PDF | Delete |
|---|---|---|---|---|---|
| Owner, same tenant | Allow | Allow | Deny | Allow | Deny |
| Finance reviewer, same tenant | Allow | Deny | Allow | Allow | Deny |
| Administrator, same tenant | Allow | Allow | Deny by separation rule | Allow | Allow |
| Ordinary user, same tenant, non-owner | Deny | Deny | Deny | Deny | Deny |
| Any user, different tenant | Deny | Deny | Deny | Deny | Deny |
| Anonymous | Deny | Deny | Deny | Deny | Deny |
Do not multiply every role by every screen without prioritization. Start with high-impact resources, irreversible actions, sensitive fields, money movement, identity changes, exports, and administrative functions. Add recently changed policy paths and endpoints reused by several clients. Risk-based selection is explained further in risk-based testing techniques.
Create a named identity for each important subject. Give fixtures obvious ownership: aliceInvoice, bobInvoice, and otherTenantInvoice are safer than three random IDs. Keep their permissions stable and create resources through trusted setup APIs or database fixtures. A negative case becomes unreliable when the test does not actually know the target owner or tenant.
For each allowed cell, include at least one positive check. For each denied cell, verify status or error code, response shape, protected field absence, unchanged resource version, unchanged related balances, and absence of downstream events where observable. This pairing prevents a false sense of safety from a feature that denies everyone because its route is simply broken.
3. Design Identities and Data for Testing Broken Access Control
Use separate credentials for each role and relationship. Never simulate authorization by editing only a role value in local storage. The server should issue or accept a real identity whose claims, memberships, and grants match the case. When tokens contain cached claims, include cases before and after a grant change, revocation, logout, and token refresh.
A compact fixture set for a multi-tenant document service could contain:
- Tenant Alpha: Alice owns document A1, Bob owns B1, Reviewer can approve within Alpha.
- Tenant Beta: Carol owns C1, another Reviewer belongs only to Beta.
- A deactivated Alpha user whose old token has not yet expired.
- A delegated service client with read scope but no write scope.
- A public document and a private document with similar names.
Change one dimension at a time. First send Alice's valid request for A1. Then keep the request identical and replace A1 with B1 to test horizontal access. Next use the Alpha Reviewer on C1 to test tenant isolation. Then use Alice on the approval operation to test vertical access. Controlled changes make a failure explainable.
Avoid production data and never guess real customer identifiers. Use an isolated authorized environment, synthetic records, rate limits, and an agreed test window. Security test payloads can trigger monitoring, account locks, or audit workflows. Coordinate expected signals with the security and operations teams without disabling the controls you intend to observe.
Capture a resource snapshot before a deny test. After the response, read the resource through an authorized identity or query a controlled audit view. Compare version, owner, status, balance, permissions, and relevant event count. A response can claim failure after a write already committed, especially when validation or authorization happens too late.
4. Test Horizontal, Vertical, and Contextual Privilege Boundaries
Horizontal privilege escalation crosses between peers. User A reads or edits User B's object by changing a path parameter, query parameter, GraphQL variable, form field, nested JSON ID, filename, or indirect reference. Test both predictable and opaque identifiers. UUIDs reduce guessing, but they do not replace authorization. IDs leak through URLs, logs, emails, browser history, exports, analytics, and related APIs.
Vertical privilege escalation invokes a function intended for a more privileged role. Send a normal user's credentials directly to administrative routes, approval operations, role assignment, support impersonation, configuration, and internal-looking endpoints. Test the documented HTTP method plus reasonable alternate methods when the router exposes them. A UI that omits the route is not evidence of server enforcement.
Contextual access depends on more than role. Verify ownership transfers, delegated access, membership removal, project moves, region restrictions, approval thresholds, time windows, and lifecycle state. A user who could edit a draft might need read-only access after submission. A former team member should lose access without waiting for a new login if the policy requires immediate revocation.
Also test confused-deputy paths. A low-privilege user may not call an admin endpoint directly but might cause a trusted worker, import, webhook, or preview service to fetch or mutate an unauthorized resource. Supply a cross-scope reference through every feature that resolves object IDs on the caller's behalf. The worker must preserve the initiating subject and scope or apply a deliberately documented service policy.
Record the policy sentence beside each case. 'Bob receives 404 for Alice's invoice and invoice version remains 7' is stronger than 'verify security.' If stakeholders cannot agree on the sentence, the requirement needs clarification before an automated test can become a trustworthy oracle.
5. Cover Object, Property, and Function-Level Authorization
Object-level authorization asks whether the caller may act on this particular instance. List and search endpoints deserve the same attention as detail endpoints. Test filters, pagination cursors, counts, autocomplete, recently viewed lists, related-resource expansions, and error messages. A response that hides records but returns a global count can still disclose business information.
Property-level authorization asks which fields a caller may read or write. A support agent might view an email but not a tax identifier. A user might edit a display name but not accountTier, ownerId, creditLimit, or isAdmin. Submit forbidden properties individually and mixed with allowed properties. The API should reject or safely ignore them according to its contract, and it must not persist them. Then read through a privileged identity to confirm.
Function-level authorization protects operations such as refund, invite, export, suspend, merge, impersonate, rotate key, and run report. Enumerate operations from route definitions, API specifications, GraphQL schema, UI network calls, mobile traffic, and job producers. Include legacy versions and endpoints that are not linked from the current client.
Mass assignment deserves a focused test. Copy a valid update payload, add a protected property, vary casing if the deserializer is case-insensitive, and try nested representations accepted elsewhere. Do not spray arbitrary fields. Use a small, reviewed list derived from the domain model and contract. Excessive fuzzing can create noisy, unsafe tests while missing the actual protected attributes.
GraphQL requires field and resolver coverage. A top-level query may be authorized while a nested resolver exposes a restricted object. Test aliases, fragments, node lookup, pagination edges, mutations, and introspection according to the deployed policy. The authorization decision should follow the resolved object and subject, not the route name alone.
6. Exercise Alternate Paths, Files, Exports, and Bulk Operations
Access checks often differ across endpoints that reach the same data. Compare web and mobile APIs, current and legacy versions, singular and bulk routes, JSON and CSV exports, preview and download paths, thumbnails, print views, and asynchronous report links. If a protected object can be referenced through another resource, test that traversal too.
Files need checks at upload, metadata read, preview, direct download, transformation, share, and deletion. A signed URL should be short-lived enough for the business risk, scoped to the intended object and operation, and unusable after revocation when the product promises that behavior. Do not place real secrets in test files. Use synthetic markers that prove whether a response belongs to Alpha or Beta.
For bulk APIs, authorization must be evaluated per item unless the contract deliberately makes the entire operation atomic. Mix allowed and denied IDs in the same request. Verify that denied records do not change, success results do not reveal protected metadata about failures, and response ordering or correlation IDs let clients match outcomes safely. See testing bulk and batch endpoints for a deeper batch model.
Exports and background jobs can create delayed exposure. Start an export as an allowed user, remove the user's membership before the job completes, and verify the documented policy at generation and download time. Test whether a guessed job ID leaks status, filenames, row counts, or the artifact. Confirm that notifications and audit records avoid sensitive content.
Finally, test method and parser consistency. A route protected for PUT may accidentally expose equivalent PATCH, form, or multipart behavior. A proxy and application may interpret path normalization differently. Keep these cases scoped to known routes and approved environments, then report the exact raw request and the component that handled it.
7. Verify Tokens, Sessions, Caches, and Permission Changes
Authorization state changes over time. Test adding and removing roles, transferring ownership, disabling an account, rotating service credentials, ending a delegated session, and changing group membership. Define when each change must take effect. Immediate revocation may require a server-side session check or short token lifetime. A product that accepts old self-contained tokens until expiry should state that residual window as a security decision.
Check both the original session and a newly issued session. Clear browser caches only when the user workflow would do so. A test that always logs in after a role change cannot detect stale token claims. Conversely, a test that expects an old access token to mutate itself misunderstands token behavior. Align the case with the architecture.
Caching can cross authorization boundaries. Vary user, tenant, role, and content negotiation while requesting the same URL through the same gateway. Responses containing private data should have appropriate cache controls, and shared caches must include the authorization-relevant key or avoid storing the response. Test a privileged request followed by an ordinary request and reverse the order.
Sessions also interact with cross-site request protections. CSRF and authorization solve different problems. A request may be authorized for the logged-in user but still be attacker-triggered through a browser. Include CSRF controls for cookie-authenticated state changes, but do not treat a valid CSRF token as permission to access another user's object.
Log review should show the effective subject, tenant, action, resource reference, decision, and a safe reason code. Logs must not contain access tokens or sensitive response bodies. A denied attempt that is invisible to operations weakens incident investigation, while a log that exposes secrets creates a different vulnerability.
8. Automate a Runnable Authorization Matrix
The following Node.js 20+ example is self-contained. It starts a local HTTP server, creates two tenant-scoped documents, and tests owner, non-owner, and cross-tenant behavior with the built-in test runner and fetch. Save it as access-control.test.mjs, then run node --test access-control.test.mjs.
import test, { after, before } from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
const documents = new Map([
['a1', { id: 'a1', tenant: 'alpha', owner: 'alice', secret: 'ALPHA-MARKER' }],
['b1', { id: 'b1', tenant: 'alpha', owner: 'bob', secret: 'BOB-MARKER' }],
['c1', { id: 'c1', tenant: 'beta', owner: 'carol', secret: 'BETA-MARKER' }],
]);
const identities = {
alice: { user: 'alice', tenant: 'alpha', role: 'user' },
bob: { user: 'bob', tenant: 'alpha', role: 'user' },
reviewer: { user: 'ruth', tenant: 'alpha', role: 'reviewer' },
};
function identityFrom(request) {
return identities[request.headers['x-test-user']];
}
function canRead(identity, document) {
return Boolean(identity) && identity.tenant === document.tenant
&& (identity.user === document.owner || identity.role === 'reviewer');
}
const server = createServer((request, response) => {
const match = /^\/documents\/([^/]+)$/.exec(request.url ?? '');
const document = match && documents.get(match[1]);
if (request.method !== 'GET' || !document) {
response.writeHead(404).end();
return;
}
if (!canRead(identityFrom(request), document)) {
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ code: 'NOT_FOUND' }));
return;
}
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(document));
});
let baseUrl;
before(async () => {
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
baseUrl = `http://127.0.0.1:${address.port}`;
});
after(async () => {
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
});
async function readDocument(asUser, id) {
return fetch(`${baseUrl}/documents/${id}`, { headers: { 'x-test-user': asUser } });
}
test('owner can read the owned document', async () => {
const response = await readDocument('alice', 'a1');
assert.equal(response.status, 200);
assert.equal((await response.json()).secret, 'ALPHA-MARKER');
});
test('peer cannot read another owner document', async () => {
const response = await readDocument('alice', 'b1');
assert.equal(response.status, 404);
assert.doesNotMatch(await response.text(), /BOB-MARKER/);
});
test('same-tenant reviewer can read but cannot cross tenants', async () => {
assert.equal((await readDocument('reviewer', 'b1')).status, 200);
const denied = await readDocument('reviewer', 'c1');
assert.equal(denied.status, 404);
assert.doesNotMatch(await denied.text(), /BETA-MARKER/);
});
A production suite should obtain identities through supported authentication, not a test header. Keep the matrix data-driven, but do not reduce every case to status comparison. Mutation tests need an authorized after-read, event check, or database reconciliation to prove no side effect. Failures should print the subject label, resource label, expected decision, actual response, and safe correlation ID.
9. Add Authorization Testing to Delivery Without Creating Noise
Run a small policy contract on every relevant change. It should cover one positive and one high-risk negative case per critical resource, tenant isolation, protected properties, and key administrative functions. Run broader matrices on scheduled builds or when the policy, identity provider, gateway, resource service, or shared authorization library changes.
Keep security fixtures isolated from general end-to-end users. Parallel workers must not transfer the same object's ownership or revoke the same membership. Generate unique resource IDs, label synthetic tenants, and clean up with a privileged test utility whose use is restricted to non-production environments. If cleanup fails, preserve the evidence without allowing stale records to influence the next run.
When reporting a defect, include the policy expectation, identity and tenant labels, minimal request, target resource relationship, response, observed side effect, reproducibility, and impact. Remove tokens, cookies, personal data, and secrets. Coordinate sensitive findings through the approved security channel rather than a broadly visible issue tracker. The API security testing guide provides complementary coverage for authentication, input handling, and service protections.
Treat an unexpected 200 as a starting point, not the whole finding. Determine whether the response exposed data, whether a write committed, whether an event ran, and whether the result is reachable through another client. Also investigate unexpected denies. A new authorization rule that blocks legitimate support or recovery work is a functional defect even when it appears secure.
Track coverage by policy rule and boundary, not by raw request count. Useful questions are: Which roles have a deny test? Which resources have cross-tenant checks? Which permission changes have stale-session coverage? Which asynchronous artifacts recheck access? That view helps a team invest in missing controls rather than celebrate a large but repetitive suite.
10. Use a Focused Broken Access Control Test Charter
A two-hour exploratory charter can complement automation: 'Attempt to access or influence Alpha invoices as an unrelated Alpha user and as a Beta user across list, detail, update, export, attachment, and job paths. Preserve evidence and avoid destructive operations beyond synthetic fixtures.'
Start with a route and resource inventory. Browse normally as each identity and save the approved network calls. Replay only against the authorized test environment, substituting controlled IDs and credentials. Explore one boundary at a time, then branch when the system reveals an alternate path such as a preview URL, event stream, or download token.
Use a test note with columns for subject, action, resource, relationship, expected decision, actual decision, side effect, and evidence. Record promising ideas you did not execute because of time or safety. This preserves residual risk and prevents later summaries from implying exhaustive coverage.
Stop and escalate if testing reaches real customer data, production-only administration, uncontrolled notification delivery, destructive shared resources, or a boundary outside the approved scope. Responsible testing is precise about authorization to test, not only authorization inside the product.
At the end, separate confirmed defects, requirement ambiguities, hardening opportunities, and untested hypotheses. A missing rate limit is not automatically an access-control defect. A verbose error with a hidden record ID may be an information-disclosure issue. Accurate classification helps the right team respond quickly without minimizing the actual impact.
Interview Questions and Answers
Q: How do you start testing broken access control?
I identify subjects, resources, actions, relationships, scopes, and states, then convert the policy into an allow and deny matrix. I create controlled identities and owned resources, test the server directly, and verify both the response and absence of unauthorized side effects.
Q: What is the difference between authentication and authorization?
Authentication establishes the caller's identity. Authorization decides whether that identity may perform a particular action on a particular resource in the current context. A valid session can still produce an authorization defect if object ownership or tenant scope is not enforced.
Q: How do you test IDOR or object-level authorization?
I send a valid owner request, then keep the identity and operation constant while replacing the resource reference with another user's controlled object. I repeat across path, query, body, nested, list, export, and indirect reference paths, and confirm no data or side effect escapes.
Q: Is 404 better than 403 for a protected object?
Either can be correct according to the product's threat model and API contract. A 404 can reduce object enumeration, while a 403 can communicate that an authenticated caller lacks permission. My primary oracle is that the object is not disclosed or changed and that behavior is consistent.
Q: Why are UUIDs not an access control?
UUIDs make guessing harder, but identifiers can leak through links, logs, notifications, exports, referrers, and related endpoints. The server must authorize every referenced object even when its identifier has high entropy.
Q: How do you verify a denied update caused no side effect?
I capture the resource version and relevant related state before the request. After denial, I read through an authorized identity or controlled data view and compare fields, version, balances, events, and audit outcome.
Q: What makes multi-tenant authorization testing different?
Tenant scope becomes an independent boundary in addition to role and ownership. I use equivalent fixtures in two tenants, cross their identifiers through direct and indirect paths, and include caches, exports, workers, and administration tools that might lose tenant context.
Q: What authorization checks belong in CI?
Stable high-risk policy cells belong in CI: owner access, peer denial, lower-role denial, cross-tenant denial, protected-field updates, and privileged functions. I add side-effect verification for mutations and use exploratory charters for new or complex trust paths.
Common Mistakes
- Checking hidden buttons but never sending the protected request directly.
- Using one administrator account for every positive and negative case.
- Changing role, tenant, resource, and method in one request, which makes failures hard to diagnose.
- Treating a guessed identifier as the vulnerability instead of verifying unauthorized access.
- Asserting only 401 or 403 while a write, event, email, or export still occurs.
- Testing detail endpoints but skipping list counts, search, files, bulk paths, and background jobs.
- Assuming opaque IDs, client-side guards, API gateways, or CSRF tokens replace resource authorization.
- Reusing mutable accounts across parallel jobs and misclassifying fixture races as security defects.
- Logging live tokens or sensitive response bodies in CI artifacts.
- Running intrusive security cases outside explicit scope or against production customer data.
Conclusion
Testing broken access control is most effective when authorization becomes an explicit, reviewable model rather than a collection of guessed URLs. Build the matrix, control identity and ownership data, cross one boundary at a time, exercise every path to sensitive actions, and verify that denial prevents both disclosure and mutation.
Begin with one critical resource this week. Map its roles, ownership, tenant, actions, and states, then automate the smallest positive and negative matrix that would detect a real policy regression. Expand from evidence, not from endpoint count.
Interview Questions and Answers
How would you explain broken access control to an interviewer?
Broken access control means the application does not enforce an intended restriction on a resource or function. A caller may be authenticated correctly yet still read another user's record, cross a tenant, change a protected field, or invoke a privileged operation. I test the server-side decision and verify no side effect occurs after denial.
What dimensions belong in an authorization matrix?
I model subject or role, resource, action, ownership or relationship, tenant or scope, and lifecycle state. For sensitive rules I also include field-level access, delegation, time, and approval thresholds. Each allowed cell needs a positive check, and each denied cell needs a negative check.
How do you test horizontal privilege escalation?
I first make a valid request as an owner, then substitute a controlled peer's resource ID without changing other inputs. I test direct, nested, list, file, export, and asynchronous references and verify the peer's data is neither exposed nor changed.
How do you test vertical privilege escalation?
I call privileged operations directly with a lower-role identity, including administrative routes, approvals, exports, role changes, and legacy versions. I also test indirect paths where a trusted worker might execute the action for the user.
What is your oracle for an unauthorized mutation?
I check the documented status and safe error shape, then verify the resource and related state through an authorized channel. Version, fields, balances, events, files, and notifications must remain unchanged except for an expected security audit record.
How would you test access after a role is revoked?
I define the required revocation timing, keep the existing session, and test before and after token refresh as well as with a new login. I include cached responses, active downloads, jobs, and delegated sessions because each may preserve old authority differently.
How do you report an access control defect safely?
I use synthetic identities and data, state the violated policy, provide a minimal sanitized request, and document response and side effect. I remove credentials and sensitive bodies and use the organization's approved security disclosure channel.
How do you prevent authorization tests from becoming flaky?
I isolate identities and tenant fixtures per worker, use known ownership, avoid shared role mutations, and reconcile state after writes. I separate policy expectations from setup failures and retain safe correlation IDs for diagnosis.
Frequently Asked Questions
What is broken access control testing?
It is testing whether every protected action and resource is limited to the identities allowed by policy. Good coverage includes roles, ownership, tenant, state, fields, functions, and indirect paths, plus proof that denied operations cause no protected side effect.
Should QA test access control through the UI or API?
Use both, but treat the server boundary as authoritative. UI checks confirm the intended experience, while direct API tests prove that hidden controls and route guards cannot be bypassed.
What test data is needed for authorization testing?
Create named identities for relevant roles plus owned resources in at least two scopes or tenants when applicable. Include owners, non-owners, privileged users, ordinary users, anonymous access, and recently revoked access.
Does every unauthorized request need to return 403?
No. An unauthenticated request commonly receives 401, and some products return 404 for an inaccessible object to reduce enumeration. Follow the documented contract and always verify that no protected data or state escapes.
How can QA test multi-tenant data isolation?
Create equivalent synthetic records in two tenants, then pass one tenant's references through the other tenant's identities across detail, list, file, export, bulk, and job paths. Also test privileged roles and caches because they often carry wider scope.
How do you automate broken access control tests?
Use a data-driven matrix of subject, action, resource relationship, expected decision, and expected side effect. Authenticate as real test identities, send requests directly, and use an authorized read or reconciliation check after denied mutations.
Are UUIDs enough to prevent IDOR?
No. UUIDs reduce simple guessing but can leak through many application channels. Every request that accepts an object reference still needs an authorization decision for the current subject and scope.