Blog

Secure code review: a practical guide and checklist

What a secure code review covers, how it fits with automated scanning, a checklist mapped to the OWASP Top 10, and what changes when AI wrote the code.

Alex Mercer

A secure code review is a read of source code aimed at finding security flaws, such as injection, broken access control, exposed secrets and risky dependencies, before the code ships. The practical version runs on every pull request that touches a trust boundary: scanners catch the known patterns first, and a reviewer checks what they can't, such as authorization logic, business rules and whether the change weakens a control that already exists.

OWASP's Secure Code Review Cheat Sheet defines it as "the process of manually examining source code to identify security vulnerabilities that automated tools often miss." A security code review uses the same pull request and the same diff as any other review. What changes is the question the reviewer asks of each line: how could someone abuse this?

If coding agents write much of your code, our guide to AI code security covers the flaws research keeps finding in AI-generated code.

What a secure code review covers

OWASP separates security-focused review from functional review by what it targets: input validation, authentication, authorization, cryptography and potential attack vectors.

OWASP describes two kinds of secure code review. Its hybrid approach combines them: baseline reviews for high-risk components, diff-based reviews for routine changes.


Diff-based review

Baseline review

Scope

The changes in a pull request or commit

The whole codebase

When

Every pull request, feature completion, hotfixes

New applications, major releases, legacy systems coming under review, compliance cycles, after an incident

First questions

What does this change do to existing security controls? What new attack vectors does it add?

Where are the entry points, trust boundaries and high-risk functions?

The diff-based steps in OWASP's cheat sheet make a good default for pull requests: analyze the impact on existing security controls, identify new attack vectors, verify security at modified trust boundaries, check new integrations, and make sure nothing regressed.

NIST's Secure Software Development Framework (SP 800-218, version 1.1, still the latest final version) makes this a named practice, PW.7. It separates code review, where "a person looks directly at the code to find issues," from code analysis, where "tools are used to find issues in code." It also asks teams to record and triage every issue found in the development team's workflow or issue tracker.

How secure code review fits with automated scanning

Each kind of check reads something different, so none of them replaces the others.

Check

What it examines

Good at

What it leaves to the reviewer

Static analysis (SAST)

Source code, without running it

Well-known flaws such as SQL injection, on every commit

Authentication, access control and crypto misuse; configuration that isn't in the code; sorting real findings from false positives

Software composition analysis (SCA)

Third-party components and their versions

Known vulnerabilities, outdated components, license terms

Whether a new dependency is needed, and whether it is the package you meant

Secret scanning

Code and config, for credential patterns

Keys and passwords committed by mistake

Revoking or rotating anything that already leaked

Dynamic testing (DAST)

The running application, from the outside

Cross-site scripting, SQL injection, command injection, path traversal, insecure server configuration

Code paths the scan never reaches, and logic that needs context

AI code review

The pull request, with context from the rest of the repository

A first pass on every pull request for logic bugs and security issues

Final judgment on risk, design and intent

Secure code review

The change, its callers and the controls around it

Business logic, authorization, cryptographic misuse, race conditions

It can't cover every line, so point it at risky changes

The table draws on OWASP's pages on source code analysis tools, DAST and component analysis. The OWASP page on static analysis is frank about its limits: it says such tools "can automatically identify only a relatively small percentage of application security flaws" and produce "high numbers of false positives."

OWASP's cheat sheet describes how the pieces fit. Run the automated tools before the manual review, use their findings to guide deeper investigation, apply human judgment to filter false positives, and focus manual effort where tools can't reach. For injection specifically, A05:2025 Injection says detection "is best achieved by a combination of source code review along with automated testing."

Secure code review checklist, grouped by risk

We mapped each group to a category in the OWASP Top 10:2025. Many items come from OWASP's cheat sheet or a Top 10 entry; others, such as passing command arguments as a list, verifying JWTs and testing denied paths, are common practice. Drop what doesn't apply to your stack and add your team's own rules. For the non-security half of a review (correctness, readability, tests), use our complete code review checklist.

Input handling and injection

Maps to A05:2025 Injection, and to A08:2025 for deserialization.

  • Inputs are validated on the server, using allowlists rather than blocklists.

  • Queries use parameters. No SQL or NoSQL is built by concatenating input, and ORM filters don't take raw user objects.

  • No shell command is built from input. Arguments go to the process as a list.

  • Output is encoded for its context: HTML, JavaScript, CSS or URL.

  • File paths from input are normalized and kept inside an allowed directory.

  • Untrusted data is not deserialized into objects, and XML parsers have external entities turned off.

  • Request bodies bind only to an allowlist of fields, to prevent mass assignment.

  • Server-side requests to user-supplied URLs are checked against an allowlist.

For language-specific patterns, see Node.js security patterns every code reviewer should enforce.

Authentication and sessions

Maps to A07:2025 Authentication Failures.

  • Passwords are hashed with a slow, salted algorithm.

  • Login, password reset and MFA endpoints block repeated failures quickly, so brute force and credential stuffing can't run unchecked.

  • Session tokens have at least 128 bits of entropy and are invalidated on logout and timeout.

  • Session cookies are set HttpOnly, Secure and SameSite.

  • Sensitive actions, such as changing an email address or password, require re-authentication.

  • Password reset links are time-limited.

  • JWT signatures and expiry are verified before any claim is trusted.

For a deeper pass on login and session code, see our security review checklist for PRs touching authentication.

Access control

Maps to A01:2025 Broken Access Control.

  • Every new endpoint enforces authorization on the server, and access is denied by default.

  • Object-level checks confirm the record belongs to the caller. Missing ones cause insecure direct object references (IDOR).

  • Admin functions are protected, and users can't change their own roles.

  • Horizontal and vertical privilege escalation are both covered: another user's data, and another role's actions.

  • CORS allows only trusted origins.

  • New cloud permissions and service accounts grant only what the code uses.

  • Authorization logic lives in one place rather than being repeated in each handler.

  • Tests cover the denied paths: no session, another user's record, a role without access.

A missing object-level check looks like this, on a route that checks who the caller is but not what they may touch:

// Before: any signed-in user can read any invoice by changing the ID
app.get("/invoices/:id", requireAuth, async (req, res) => {
  const { rows } = await db.query("SELECT * FROM invoices WHERE id = $1", [
    req.params.id,
  ]);
  res.json(rows[0]);
});

// After: the query only matches invoices in the caller's account
app.get("/invoices/:id", requireAuth, async (req, res) => {
  const { rows } = await db.query(
    "SELECT * FROM invoices WHERE id = $1 AND account_id = $2",
    [req.params.id, req.user.accountId]
  );
  if (rows.length === 0) return res.sendStatus(404);
  res.json(rows[0]);
});
// Before: any signed-in user can read any invoice by changing the ID
app.get("/invoices/:id", requireAuth, async (req, res) => {
  const { rows } = await db.query("SELECT * FROM invoices WHERE id = $1", [
    req.params.id,
  ]);
  res.json(rows[0]);
});

// After: the query only matches invoices in the caller's account
app.get("/invoices/:id", requireAuth, async (req, res) => {
  const { rows } = await db.query(
    "SELECT * FROM invoices WHERE id = $1 AND account_id = $2",
    [req.params.id, req.user.accountId]
  );
  if (rows.length === 0) return res.sendStatus(404);
  res.json(rows[0]);
});
// Before: any signed-in user can read any invoice by changing the ID
app.get("/invoices/:id", requireAuth, async (req, res) => {
  const { rows } = await db.query("SELECT * FROM invoices WHERE id = $1", [
    req.params.id,
  ]);
  res.json(rows[0]);
});

// After: the query only matches invoices in the caller's account
app.get("/invoices/:id", requireAuth, async (req, res) => {
  const { rows } = await db.query(
    "SELECT * FROM invoices WHERE id = $1 AND account_id = $2",
    [req.params.id, req.user.accountId]
  );
  if (rows.length === 0) return res.sendStatus(404);
  res.json(rows[0]);
});

A review comment might say: "requireAuth confirms the caller is signed in, not that this invoice is theirs. Scope the query by account_id and return 404 when nothing matches."

Secrets and cryptography

Maps to A04:2025 Cryptographic Failures.

  • No keys, tokens or passwords in code, tests, fixtures or config. Secrets come from a secret store and can be rotated.

  • No MD5 or SHA-1 for anything security-related, and no homegrown cryptography.

  • Tokens, reset codes and IDs that must be unguessable come from a cryptographically secure random generator.

  • IVs and nonces follow the rules of the cipher mode: unpredictable for CBC, unique per key for GCM.

  • TLS certificate and hostname validation stay on.

  • Every key has an owner, a storage location and a rotation plan.

If a real secret was committed, revoke or rotate it before anything else. Removing it from the code does not remove it from git history.

Dependencies

Maps to A03:2025 Software Supply Chain Failures and A08:2025 Software or Data Integrity Failures.

  • Each new dependency is needed, exists on the registry under that exact name, is maintained, and comes from an official source.

  • Versions are pinned in a lockfile, and lockfile changes are read, including transitive dependencies.

  • New and updated versions are checked against CVE, NVD or OSV data.

  • Changes to CI/CD workflows get the same review as application code.

A03:2025 also states the principle behind requiring review at all: "No single person should be able to write code and promote it all the way to production without oversight from another human being."

Logging and errors

Maps to A09:2025 Security Logging and Alerting Failures and A10:2025 Mishandling of Exceptional Conditions.

  • Logins, failed logins and high-value transactions are logged.

  • No passwords, tokens or personal data end up in logs, and log data is encoded so it can't inject into logging or monitoring systems.

  • Error responses don't expose stack traces or internal details.

  • Errors fail closed. A failed check denies access, and a failed multi-step transaction rolls back completely.

How to run a secure code review on a pull request

  1. Read the description and the file list first. Mark what crosses a trust boundary: new endpoints, new inputs, new outbound calls, changed permissions.

  2. Read the scanner output before the code. Triage it, and note which areas the tools can't judge.

  3. Trace data from source to sink. Follow each new input through validation and business logic to where it lands: a query, a file, a response, a log line.

  4. Check the controls the change touches as well as the new lines. A small edit to a middleware or a shared helper can weaken every route that uses it.

  5. Think like an attacker. Ask how the feature could be misused: skipped steps, replayed requests, another user's IDs, concurrent requests.

  6. Write findings someone can act on. OWASP's finding template covers a title, severity, CWE, location, impact, reproduction steps and a recommended fix.

  7. Record and triage everything. Following NIST's PW.7, findings go into the team's tracker, including the low-severity ones.

Using the OWASP Code Review Guide and OWASP Top 10

The OWASP Code Review Guide is OWASP's book for people who review code. Its first section covers why and how to review. The second covers types of vulnerabilities and how to spot them, with examples of what not to do and what to do. The appendix includes a reviewer checklist. The current PDF dates from July 2017, and its vulnerability chapters mostly follow the OWASP Top 10 from 2013.

Use the guide for method. For current risk categories, use the OWASP Top 10:2025. For a current checklist, use the Secure Code Review Cheat Sheet.

OWASP Top 10:2025

Where it shows up here

A01 Broken Access Control

Access control

A02 Security Misconfiguration

Access control (cloud permissions), logging and errors (stack traces in error responses)

A03 Software Supply Chain Failures

Dependencies

A04 Cryptographic Failures

Secrets and cryptography

A05 Injection

Input handling and injection

A06 Insecure Design

Mostly a design-stage concern; step 5 of the process (thinking like an attacker) catches some of it

A07 Authentication Failures

Authentication and sessions

A08 Software or Data Integrity Failures

Dependencies, and deserialization under input handling

A09 Security Logging and Alerting Failures

Logging and errors

A10 Mishandling of Exceptional Conditions

Logging and errors

Our OWASP Top 10 guide covers each category in more depth.

What changes when AI wrote the code

The checklist stays the same. The emphasis shifts, because research keeps finding the same weaknesses in AI-generated code:

  • Injection gets a closer look. In Perry et al.'s user study (ACM CCS 2023), 36% of participants with an AI assistant wrote SQL-injectable code, against 7% without one. The assistant often concatenated strings instead of using prepared statements.

  • Every new dependency gets checked. Spracklen et al. (USENIX Security 2025) found that 19.7% of the package references generated by 16 models named packages that don't exist. Many of those names repeat across runs, so attackers can register them in advance.

  • Secrets get searched for. Code completion tools can reproduce credentials from their training data (Huang et al., FSE 2024).

  • Old defaults get questioned. In Pearce et al.'s study (IEEE S&P 2022), Copilot often hashed passwords with MD5 or a single round of SHA-256.

  • The author's confidence counts for less. Perry et al. also found that participants with an AI assistant were more likely to believe they had written secure code. Ask which parts an agent wrote and whether the author has read them, and don't let an agent approve or merge its own change.

Rates also differ by model. On cubic's Bug Bench, the seven models ranked for security and privacy bugs run from 0.56 for GPT-5.6 Sol, 44% below average, to 1.50 for Claude Opus 5.5, 50% above the all-model rate. The rates are observational: they reflect the code each model was used for as well as the model.

Volume is the other change. When agents open more pull requests than people can read closely, an AI reviewer can take the first pass on each one. As a GitHub app, cubic reviews every pull request in minutes and leaves inline comments with suggested fixes, so human reviewers can spend their time on design and intent. Our guide to AI code review covers how to roll one out and measure it.

Parts of the checklist can also run as written review rules. Flag Security Vulnerabilities covers the injection, secrets and weak-crypto items, Require API Auth, Validation, and Tenant Scoping covers missing auth checks and client-supplied tenant or account IDs, and Prevent Sensitive Data in Logs covers the logging item. Paste one into AGENTS.md and your coding agent follows it while it writes code.

Common mistakes in secure code reviews

  • Treating a clean scan as a clean review. OWASP lists authentication problems, access control issues and insecure use of cryptography as hard for static analysis to find automatically.

  • Reading only the changed lines. The first diff-based step in OWASP's cheat sheet is to analyze the impact on existing security controls, and that means reading callers and shared code too.

  • Checking dependencies at release time. Review a dependency in the pull request that adds it, while removing it is still cheap.

  • Leaving findings in chat. NIST's PW.7 asks teams to record and triage discovered issues in their workflow or issue tracker, so findings survive the conversation and show up in audits.

  • Dropping low-severity findings without triage. Triage everything, then decide. A low-severity bug can still matter in combination with another one.

FAQ

What is a secure code review?

It is a review of source code that looks specifically for security flaws, such as injection, broken access control, exposed secrets and vulnerable dependencies, before the code ships. OWASP describes it as manually examining source code to find vulnerabilities that automated tools often miss. It works best on every pull request, with periodic full-codebase reviews for high-risk components.

Are secure code reviews worth it?

For code that handles authentication, authorization, payments, personal data or untrusted input, yes. OWASP notes that static analysis tools find only a relatively small percentage of application security flaws, and that access control and authentication problems are hard to detect automatically. Keep the cost down by reviewing small pull requests as they land rather than auditing a whole release at the end.

Is AI-generated code safe to merge without review?

No. Peer-reviewed studies have found vulnerabilities in a large share of AI-generated code, including about 40% of the programs GitHub Copilot wrote for high-risk scenarios in one study. Review it with the same checklist, with extra attention to injection, new dependencies and hard-coded secrets.

Which SAST tool is best?

None is best for every team. Options include GitHub's CodeQL, Semgrep Code, Snyk Code and SonarQube Server, and OWASP lists many more, without endorsing any, on its page of source code analysis tools. Pick one that supports your languages and frameworks, runs on every pull request and produces findings your team fixes, then compare real findings against false positives on code you know. Whichever you pick, it won't judge authorization or business logic for you, so it supports a secure code review rather than replacing one.

Table of contents