Security review guide
How do you review an access-control code change safely?
Published
Quick answer
Review an access-control change by writing the decision before judging the code: which requesting subject may perform which action on which resource, under which context, and at which trusted layer is that rule enforced? Trace every known entry path to the protected operation. Then test one allowed case and nearby denied cases across function, object, field, and tenant boundaries. Hidden controls, hard-to-guess identifiers, and a passing happy path are not authorization evidence. This workflow narrows review risk; it does not certify the system as secure.
Write the authorization decision before reading role checks
Authentication establishes who or what is making a request. Authorization decides whether that subject may take this action on this resource in the current context. A valid session answers the first question, not the second. Public operations may intentionally allow unauthenticated access, but that outcome should still follow an explicit access rule. Start the review by writing the expected allow or deny decision in plain language before the implementation's role names shape your assumptions.
Use five fields: subject, action, resource, context, and trusted enforcement point. Add ownership, tenant, relationship, workflow state, or time constraints when they affect the result. If the required rule cannot be stated clearly, record that ambiguity as a review finding rather than treating the current condition as the contract.
Trace every known route to the protected operation
List the entry paths that can reach the effect: HTTP handlers, GraphQL resolvers, background jobs, internal services, import tools, scheduled work, and administrative adapters where they exist. Follow each path until you find the component that has both the trusted subject context and the resource data needed to make the decision. A check in one controller does not protect another caller automatically.
Consider a fictional archiveProject operation called by both an HTTP endpoint and a queued maintenance command. A proposed patch adds isManager to the HTTP handler only. The review is not complete until it explains the second path, distinguishes any intermediary service identity from the originating subject, and shows where the system proves that this manager belongs to this project and tenant before archival begins.
Review the decision at the correct granularity
Function-level access asks whether the subject may invoke the operation at all. Object-level access asks whether it may act on this particular record. Field-level access separates attributes that may be read from those that may be changed. Tenant-level access prevents an otherwise valid role from crossing an isolation boundary. A broad role check can pass while any one of these narrower decisions is still wrong.
Context can narrow the rule further: a project state, approval stage, relationship, delegated scope, or time window may change the outcome. Review the rule the application actually promises instead of forcing every system into one RBAC, ABAC, or relationship-based model.
Test denied neighbours, not only the happy path
Begin with one clearly allowed subject-resource pair, then change one meaningful fact at a time. Try a lower-privilege subject, a same-role subject acting on another owner's resource, and a same-role subject from another tenant. Add missing, expired, or stale context when the rule depends on it. These nearby cases are more informative than a random collection of invalid requests because each one falsifies a specific part of the decision.
For the archiveProject example, the strongest small test set includes the intended project manager, a manager for a different project, and a manager from a different tenant. Exercise materially different entry paths as well. Keep the fixtures synthetic and verify the decision before any archive state, message, or external request changes.
Check what happens around the decision
Authorization must happen before the protected side effect. Trace writes, emitted messages, external calls, and cache changes around the denial path. A response that says denied is not enough if the operation already changed durable state or started work that cannot be recalled.
Also review how permission changes reach long-lived tokens, queues, workers, and caches. The right propagation rule depends on the system, but the change should make its expiry, invalidation, re-check, or reconciliation behavior explicit. Keep audit evidence useful without logging credentials, bearer tokens, private payloads, or unnecessary identifiers.
Know what this review cannot establish
A focused pull-request review can expose missing decisions, misplaced enforcement, and weak denied-path evidence. It cannot enumerate every reachable path in an unknown architecture, prove that a framework is configured correctly, or establish that no bypass exists. Passing the listed cases supports those cases only.
Escalate when the change introduces a new trust boundary, delegation model, cross-tenant workflow, high-impact administrative action, or uncertain identity propagation. Threat modelling, framework-specific guidance, dynamic security testing, and specialist review answer broader questions than this code-reading workflow can settle.
An access-control review decision table
Use this original decision aid to turn a plausible check into a reviewable claim. Each row names the evidence still needed before the change can be approved; it is not a universal architecture or compliance checklist.
| Observed condition | Review decision | What to verify |
|---|---|---|
| A handler checks a role but accepts a resource identifier | Do not approve from the role check alone. | Show a policy decision for this action on the loaded resource, plus same-role other-owner and cross-tenant denied cases where those boundaries exist. |
| The operation is reachable through more than one adapter | Verify equivalent enforcement on every path or at a trusted shared layer. | Provide an entry-path map and focused evidence for each materially different route to the protected effect. |
| The interface hides the action or identifiers are hard to guess | Treat both as defense in depth, not authorization. | A direct request from a subject without permission is rejected before any protected side effect. |
| A new field or action is added to an existing resource | Do not assume whole-object access covers the new capability. | Separate read, write, and action decisions match the documented contract and have relevant denied cases. |
| Permissions can change while tokens, jobs, or caches remain live | Require a defined stale-authority strategy. | Show that permission changes take effect immediately where feasible; otherwise document the intended expiry, invalidation, re-check, or reconciliation behavior with a revoked or changed-permission scenario. |
| The denial check runs after a write or message starts | Block approval until the ordering or atomicity is justified. | Demonstrate that denial leaves no protected state change and that any audit trail is intentional and privacy-safe. |
Interactive reasoning check
Try the review: what blocks approval?
Read the reasoning without answering
The review starts with the subject, action, resource, context, and enforcement point—not with the presence of a role check. Because two entry paths reach archiveProject, approval needs either equivalent checks on both paths or one trusted shared decision. The useful denied cases change ownership or tenant while holding the role constant, and each denial must happen before archival, messages, or other protected effects begin.
A small practice loop
- 01Restate one synthetic access-control change as subject, action, resource, context, expected decision, and trusted enforcement point before reading its implementation.
- 02Trace every known caller to the protected operation and mark where each path obtains subject and resource context.
- 03Predict one allowed result and at least three materially different denied results, including an ownership or tenant boundary when the system has one.
- 04Run or write focused tests and inspect whether any denied path changes state, sends a message, or starts an external action.
- 05Record unresolved assumptions and escalate decisions that depend on architecture, identity propagation, or a wider threat model instead of guessing.
Common questions
Is checking a user's role enough for authorization?
Not by itself. A role can be one input, but the decision may also depend on the requested action, the particular resource, ownership or tenant boundaries, fields, relationships, and current context. Review the complete decision the application promises.
Do passing denied-path tests prove the access control is secure?
No. They provide evidence for the paths and cases exercised. They do not prove that every entry path, configuration, identity transition, or bypass has been found. Broader risk can require threat modelling, dynamic testing, and specialist review.
Limits of this guide
This is not a penetration test, security audit, compliance certification, threat model, or substitute for framework-specific guidance. It does not prescribe one RBAC, ABAC, ReBAC, service-layer, or HTTP status-code design. Passing the suggested cases cannot prove the absence of other paths or bypasses. The Unrust diagnostic is a timed snapshot of general unassisted code reasoning; it does not measure security-review competence or production readiness, certify access-control implementations, or explain why a developer received a result.
Sources and scope
This page is an original editorial synthesis by Unrust. Its authorization principles were checked against OWASP guidance and ASVS 5.0, its object-level boundary against the OWASP API Security Top 10, its weakness terminology against MITRE CWE-862, and its policy-verification framing against NIST SP 800-192. The five-field workflow, fictional archiveProject example, and decision table were created for this guide rather than copied from those sources.
- Authorization Cheat Sheet
OWASP Cheat Sheet Series
Supports least privilege, deny-by-default design, authorization checks on requests, explicit trust-boundary thinking, and tests for authorization logic. It is general guidance, not application-specific assurance.
- OWASP ASVS 5.0, V8: Authorization
OWASP Application Security Verification Standard
Defines verification requirements for documented function, data, field, and contextual rules; trusted enforcement; tenant isolation; and changed permissions. The applicable ASVS level and architecture remain system-specific.
- API1:2023 Broken Object Level Authorization
OWASP API Security Top 10
Explains why an endpoint acting on an identified object must check permission for the requested action on that object, and recommends unpredictable identifiers as defense in depth alongside object-level checks. Its scope is API object-level authorization.
- CWE-862: Missing Authorization
MITRE Common Weakness Enumeration
Defines missing authorization, includes horizontal access between similarly privileged users, and notes limits of automated analysis for authorization business logic. It is a weakness taxonomy, not a prevalence or exploitability measure.
- NIST SP 800-192: Verification and Test Methods for Access Control Policies/Models
National Institute of Standards and Technology
Supports verification and testing when access-control policies, models, and implementation rules may be incomplete or inconsistent. It addresses formal policy and model assurance rather than a modern pull-request checklist.
Check your own starting point
Unrust uses short, timed, unassisted drills to give developers a per-skill snapshot. One session cannot explain the cause of a result, but it can show you what is worth practicing next.
Take the free diagnostic