Start from what the change lets someone do, then follow one request through the real code: where the input comes from, which identity and permission checks run, and when the first effect or byte is released. Compare that with the complete rule, not a partial guard. Write findings as trigger, broken rule, consequence and fix, and close them with a test that fails on the old code. Review AI-generated code the same way, and verify every dependency it adds.
- Review the capability a change adds, not the lines it touches; the missing check is often outside the diff.
- A helper name is not evidence: read what
requireUseror a tenant filter actually checks. - The decision must run before the first byte, write or queued job, including on error and cache paths.
- A good finding names the trigger, the violated rule, the consequence, the location and the repair.
- AI-generated code needs the same evidence: check that every suggested package exists and is the intended one.
- Route changes to authentication, authorization, crypto, parsers and payments to a required security reviewer.
- For each new route or job, write the request path from source to effect and mark where identity, permission and validation are decided.
- Run the reproducer against the old revision and the fix: the old code must fail the test and the new code must pass it.
- Keep one allowed case in the same test so a fix that denies everyone also fails.
- Check new dependencies against the registry and the lockfile, and reject names that do not exist or were published days ago.
- Confirm
CODEOWNERSand branch rules require the right reviewer for the sensitive paths the change touched.
- Applies to
Reviewing a specific change and its reachable call paths. A diff-only review can miss inherited controls or bypasses.
- Part of
- Workflows: Review a design, Test an application
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Review changed capabilities through actual control flow.
- Distinguish complete permission from partial guards.
- Write findings that directly become meaningful regressions.
Before you start
Developers and reviewers who approve pull requests, including changes written with AI assistants, and the security engineers they escalate to.
- Read the changed handler and identify callers, sensitive operations and data sources.
Refresh first: Review tenant boundaries before implementation
Go to the guided practiceA security review begins with what a change lets an actor do. File counts and suspicious keywords are useful navigation aids, but do not define the security boundary. If a pull request adds report download, identify the caller, report id, data source, authorization decision and response path. Read enough surrounding code to know which middleware actually applies. A helper named requireUser is evidence of a name, not proof that it checks the required permission.
The supplied patch adds GET /reports/:id/download. It authenticates a caller, loads a report by request id and streams its storage object. Reports have tenant and owner fields. The intended rule is current membership in the report's tenant and either ownership or a report_admin role within that tenant. The change makes private object bytes reachable, so the important question is whether that exact permission is enforced before the first streamed byte; /domains/appsec/authorization/object-level-authorization/ covers that rule in depth.
Not every pull request needs a security specialist, but some always do. Mark the sensitive paths in CODEOWNERS and use branch protection or rulesets that require a code owner review, so a change to authentication, session handling, authorization policy, cryptography, parsers, payments or secrets handling cannot merge on an ordinary approval. NIST's Secure Software Development Framework names this activity as practice PW.7, review and analyze human-readable code, and expects findings to be tracked to closure. Use short checklists per bug class rather than one long generic list.
| The change touches | Ask | ASVS 5.0 chapter |
|---|---|---|
| A new route, job or export | Is the full object, function and field permission checked before any effect? | V8 Authorization |
| Queries, commands, templates or parsers | Does each value reach its interpreter through a data interface? | V1 Encoding and Sanitization |
| Uploads or archives | Are type, size, path and serving rules enforced? | V5 File Handling |
| Login, sessions or tokens | Are expiry, revocation and binding unchanged or stronger? | V7 Session Management, V9 Self-contained Tokens |
| Errors and logs | Do failures deny, and do logs avoid secrets? | V16 Security Logging and Error Handling |
| Secrets and configuration | Are credentials read from a secret store and debug settings off? | V13 Configuration |
| Dependencies | Is each new package real, maintained and pinned in the lockfile? | V15 Secure Coding and Architecture |
Mark sources, transformations, decisions and effects. The route id is attacker-controlled. Session middleware supplies verified identity. The repository selects a report. The policy should evaluate the selected report's actual tenant and owner. Storage retrieval is an internal operation, but releasing the stream is the externally observable effect. Error handlers and caches are alternate paths through this sequence and deserve the same attention.
Ask what the helper guarantees. A tenant filter may prevent cross-tenant access but still allow every member to read another member's private report. An ownership check may allow an owner whose membership has been revoked. A cached response may bypass both. For a write, examine whether checked state can change before commit. Review the security-relevant lines outside the diff when they determine these semantics; the changed lines alone can hide an old helper's missing premise.
The Express module below is the repaired route. It loads the report, applies the existing complete policy and returns the same 404 for missing and forbidden reports, and only then opens the storage object. Its test runs the six requests from the exercise through real HTTP and records every object opened: Alice gets ALICE, Cara gets BOB, Dan gets DAN, the other three get 404, and only the three permitted objects were ever opened. The same test run against the original handler receives BOB for Alice, which is the evidence that the regression would have caught the flaw. The express-api project below applies the same review to a larger API.
// reports.mjs (Express 5): the repaired download route from the review.
import express from 'express';
export function canDownload(caller, report) {
return (
caller.active === true &&
caller.tenant === report.tenant &&
(caller.id === report.owner || caller.role === 'report_admin')
);
}
// req.caller is set by the session middleware from verified server state.
export function reportRoutes({findReport, openObject}) {
const router = express.Router();
router.get('/reports/:id/download', async (req, res) => {
const report = await findReport(req.params.id);
if (!report || !canDownload(req.caller, report)) {
return res.status(404).json({error: 'not_found'}); // same answer for missing and forbidden
}
const stream = await openObject(report.objectKey); // opened only after the decision
res.type('application/pdf');
stream.pipe(res);
});
return router;
}A useful finding states trigger, violated rule, observed or traced consequence, affected location and a concrete repair. For example: Alice can request Bob's report id under a valid red session; the handler streams the selected object without canDownload; this discloses Bob's private report; enforce the helper before streaming and cover same-tenant nonownership as well as cross-tenant access. That is stronger than 'missing access control' because it explains the exact missing decision.
Distinguish confirmed behavior from inference. If the review only traced source, say the route appears to release bytes because no later check exists in the inspected path. If a controlled test returned the synthetic document, record that evidence. Avoid demanding broad refactors as a condition for repairing a narrow flaw unless the architecture makes consistent enforcement impossible. A focused, reliable guard and regression can be more reviewable than a new policy engine introduced under time pressure.
Treat automated and AI-assisted review as inputs. Verify generated APIs, security claims, dependency choices and negative tests against the actual project. An assistant's confident explanation or a clean scanner output cannot establish that business permissions are correct, and generated code tends to reproduce the most common pattern, which for this route is the unguarded one. Avoid sending proprietary source or secrets to unapproved external review services.
Check every dependency an assistant adds. A USENIX Security 2025 study of 576,000 generated code samples found that on average at least 5.2% of the packages suggested by commercial models and 21.7% of those suggested by open-source models did not exist, and an attacker can register such a name and wait for someone to install it. Confirm the package is the intended project, has real history and maintainers, and is pinned in the lockfile (the intake checks in /domains/supply-chain/dependencies/project-health-and-intake/), and apply the same review and tests to generated code as to any other change.
Reviewing the fix means rerunning the original trigger and checking allowed behavior. Alice's own report should still stream its exact synthetic bytes; Bob's report and a foreign tenant report should release none. A 404 after headers or part of the body were sent is too late. Verify missing ids and repository errors do not skip the guard. Check list, preview and export routes if they share the vulnerable helper or capability. The IDOR lab at /labs/authorization/ practices the same repair and check.
Keep the final review record small: invariant, relevant call path, finding status, regression evidence and unresolved assumptions. An unresolved question about middleware order should be answered through code or a route test, not marked safe because the framework usually handles it. Security review complements automated analysis by interpreting business rules and scope; it should produce maintainable tests and better interfaces so the next reviewer need not rediscover the same boundary.
Heartbleed (CVE-2014-0160) was a missing bounds check in OpenSSL's TLS heartbeat handling that let attackers read process memory, including private keys, and shipped for two years. It is the standard argument for security-focused review and tests around memory and length handling.
A test named denies_foreign_report can still miss the defect. If its fixture accidentally makes Alice the owner of both reports, or its mock returns null for every requested id, the test may pass without exercising the real policy. Inspect the actor and object relationships, route invocation, status assertion and released-byte assertion. The strongest small regression demonstrates the old handler returns BOB and the repaired handler releases zero BOB bytes, while ALICE still succeeds through the same middleware stack.
Mocks should isolate external cost without replacing the security decision. Stubbing storage with fixed synthetic bytes is reasonable; stubbing canDownload to always deny does not verify the actual owner/tenant/admin predicate. A unit test of that predicate and a route-level test showing it is called before streaming answer complementary questions. Neither should be presented as complete evidence for the other.
Also review what the patch does on absence and exceptions. If findById returns null, the route should follow the documented missing-resource behavior without dereferencing it or leaking internals. If storage.open fails after authorization, the result is an availability error, not a reason to retry through an unauthenticated download URL. These paths often reveal whether the new guard is structurally part of the operation or only inserted into its happy path. Record remaining assumptions explicitly so a future change can identify what evidence must be refreshed.
Guided practice
Review a supplied download handler using complete identity and response evidence.
Supplied scenario material
Callers: Alice=(A,red,member,active); Cara=(C,red,report_admin,active); Dan=(D,blue,report_admin,active).
Reports: R1=(red,A,bytes ALICE); R2=(red,B,bytes BOB); R3=(blue,D,bytes DAN).
Original handler: caller = requireSession(); report = repository.findById(request.id); response.stream(storage.open(report.objectKey)).
Existing helper, not called: canDownload(caller, report) = caller.active AND caller.tenant == report.tenant AND (caller.id == report.owner OR caller.role == 'report_admin').
Missing report -> 404.
Requests: Alice/R1; Alice/R2; Cara/R2; Cara/R3; Dan/R3; Alice/missing.
Proposed patch P1: stream first, then call canDownload.
Proposed patch P2: check caller.tenant == report.tenant only.
Proposed patch P3: require canDownload before opening response stream.1. Trace the original disclosure
Follow Alice/R2 from request id to released bytes.
Expected result and reasoning
The valid session establishes A, repository selects R2 and storage returns BOB. No helper is called, so Alice receives BOB despite lacking ownership or
report_adminrole.2. Evaluate partial repairs
Decide P1 and P2 using the earliest effect and complete rule.
Expected result and reasoning
P1 checks after disclosure and fails. P2 prevents Cara/R3 but still allows Alice/R2; it enforces tenant scope only. Neither satisfies the complete rule.
3. Compute P3's access matrix
Apply
canDownloadto all six requests and give exact response content.Response under patch P3
Expected result and reasoning
Alice/R1 -> ALICE; Alice/R2 -> 404, no bytes; Cara/R2 -> BOB; Cara/R3 -> 404, no bytes; Dan/R3 -> DAN; missing -> 404, no bytes.
4. Write a review finding and closure test
State the root cause, minimal change and a test that would have failed before P3.
Expected result and reasoning
The route skips object permission before streaming. Call the existing complete policy before response effects. Alice/R2 must produce 404 and zero BOB bytes while Alice/R1 still returns ALICE. That paired test establishes denial and preserved functionality.
Paper exercise: callers, reports and patches are supplied. reports.mjs and its test above run the six requests against the repaired and the original handler.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Secure a real HTTP API
Run Express requests with real signed tokens. Repair invoice authorization and stop user-controlled properties from changing roles or tenants.
Needs: Internet access for npm ci, which downloads the pinned packages; the tests run on localhost only.
In the extracted folder, run:
npm ci --ignore-scripts
npm test14 regression tests · The starter intentionally fails some cases.
Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image, and includes Semgrep rules with their own tests in semgrep/.
Part of the learning path Developer foundations
Localhost tests with synthetic identities and in-memory records; includes actual JOSE verification.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- OWASP Secure Code Review
- OWASP ASVS 5.0.0 release
- NIST SP 800-218: Secure Software Development Framework 1.1. Version 1.1 (February 2022).
- GitHub: about code owners
- Spracklen et al.: We have a package for you! Package hallucinations by code-generating LLMs (USENIX Security 2025)
- NVD: CVE-2014-0160, OpenSSL Heartbleed
Reference information
- SDLC phase
- DevelopmentTesting
- ASVS 5.0.0 requirements
- v5.0.0-8.2.2 · v5.0.0-8.3.1