Secure code review, and what a reviewer looks at first
Reading the code top to bottom does not work and nobody experienced does it. A useful review is a search for a small number of decisions in a large amount of text, and most of those decisions are visible from three or four files. The skill is knowing which files, and knowing that the most valuable findings are absences: the check that was never written, the route that was registered without a guard, the query missing one clause.
Why is line-by-line review the wrong start?
Because it spends the reviewer's scarce attention on the categories a machine already covers, and exhausts it before reaching the categories a machine cannot. Static analysis is better than any human at finding a string concatenated into a query across ten thousand files. A human is the only thing that can notice that an endpoint returning invoices never restricts them to the caller's organisation. Sequential reading arrives at the interesting question with nothing left.
It is also poorly matched to how codebases fail. Vulnerabilities cluster at boundaries, where data crosses from untrusted to trusted, from one privilege level to another, or from one service to another with an assumption of trust attached. The interior of a well-factored module is rarely where the problem is, and reading it in order means reading mostly interior.
So the first hour goes on enumeration rather than reading. What are the entry points, what identity does each run as, and where is the decision made about what that identity is allowed to touch. If a reviewer cannot answer those three after an hour, that is itself the finding, because an application whose trust boundaries are not discoverable by a competent reader is not one whose authorisation can be relied upon.
What does a reviewer open first?
The route table, or whatever the framework's equivalent is, because it is the inventory of ways in. Then the middleware or filter configuration that decides which of those routes require authentication, and in what order it runs. The specific thing to look for is whether the default is deny: can a developer add a route and have it be public by accident, or does registration without an explicit policy fail? Frameworks differ, and the answer determines how much of the rest matters.
Entry points are wider than HTTP and the forgotten ones are where problems live. Message queue consumers, scheduled jobs, webhook receivers, GraphQL resolvers that bypass the REST middleware, internal service endpoints that assume the network is trusted, administrative command line tools, and file import paths. Each of these is a place where input arrives, and several of them commonly have no authorisation layer at all because the network was assumed to be the boundary.
Then the data access layer, looking for how a query is scoped to a tenant or an owner. The best answer is that scoping is structurally impossible to omit, because every query goes through a repository that requires an actor. The common answer is that each handler adds a filter by hand, in which case the review becomes an exercise in finding the handler where somebody forgot.
Which checks give the most per minute?
A short list of searches, each of which is a one-line query against the repository and each of which routinely finds something. None requires understanding the whole application, which is why they are the right way to begin an unfamiliar codebase.
| Area | What to search for | What a problem looks like | Rough time |
|---|---|---|---|
| Object-level authorisation | Handlers that fetch by an identifier taken from the request | A fetch by primary key with no owner or tenant predicate | An hour, highest yield |
| Mass assignment | Request bodies bound straight onto a model or entity | A role, tenant or price field settable by the caller | Twenty minutes |
| Raw query escapes | The ORM's raw or literal helpers, and string building near SQL | Interpolated identifiers, sort columns or filter fragments | Twenty minutes |
| Deserialisation and templating | Unsafe loaders, polymorphic type resolution, disabled autoescaping | User data reaching a loader that can construct arbitrary types | Thirty minutes |
| Server-side requests and file paths | Outbound fetches and path joins that include request data | A URL or filename the caller influences with no allowlist | Thirty minutes |
| Container and build configuration | The Dockerfile, build arguments and copied files | Root user, secrets passed as build arguments, environment files copied in | Ten minutes |
What does a good reviewer look for that tools cannot see?
Check-then-act sequences on anything that represents value. A handler that reads a balance, decides it is sufficient, then writes a decrement, is correct in a single-threaded world and wrong under concurrency. The fix is a database-level guarantee, whether a conditional update, a row lock or a constraint, and the absence of one is invisible to a scanner because every individual statement is fine.
Cryptographic misuse in the small. Random values used as security tokens but drawn from a general-purpose random generator rather than a cryptographic one. Comparisons of secrets with an ordinary equality operator instead of a constant-time function. A password stored with a fast general hash rather than a purpose-built password hash with a work factor. Each is a few characters of code and each defeats the control it appears to implement.
And the state machine. Ask which order the steps of a paid or privileged workflow must occur in, then look for whether anything enforces it. If the answer is that the front end only offers the buttons in the right order, an attacker who calls the endpoints directly can skip payment, reopen a closed dispute, or resubmit an approval, and no tool will report it because each endpoint does exactly what it says.
What should be reviewed on every pull request?
A change-shaped list rather than a full review, and it should be short enough that people actually apply it. Does this change add a route, a consumer or a job? Does it change who can call something? Does it introduce a new query, a new outbound request, a new file path, a new deserialiser, or a new dependency? If all six answers are no, security review adds nothing and should not be requested.
The value of framing it this way is that it makes the trigger objective. Teams that ask for security review on everything get performative approvals; teams that ask on nothing get surprised. A rule tied to the presence of specific constructs in the diff, ideally enforced by a code-owners file that pulls in a reviewer when particular paths change, generates review requests that are worth someone's time.
Pair it with one standing question in the pull request template: what could a malicious authenticated user do with this endpoint that the feature does not intend? Written answers to that question, from the author, are consistently more useful than a reviewer's guess, because the author knows the intent and the reviewer is reconstructing it.
When is external review worth paying for?
When the code implements a trust decision that is expensive to get wrong and rare enough that your team has done it once. Authentication flows, multi-tenant isolation, payment and settlement logic, permission models, licence or entitlement enforcement, and anything cryptographic. These are the areas where experience compounds and where a reviewer who has seen twenty implementations sees the failure immediately.
It is poor value for general codebase sweeps, because an external reviewer without domain knowledge cannot tell an intentional design from a mistake, and the report comes back full of questions rather than findings. If you want breadth, buy targeted testing against a running system instead. If you want depth on a boundary, buy review with the design document attached.
The practical arrangement that works is a scoped review of named files plus a walkthrough with the engineer who wrote them. An hour of conversation before the reading starts changes what the reviewer can find, because most of the interesting questions are about intent, and intent is not in the repository.
Common questions
- What does a secure code reviewer look at first?
- The route table and the middleware that decides which routes require authentication, then the data access layer to see how queries are scoped to a tenant or owner. The key question is whether the default is deny, meaning a developer cannot add a public endpoint by accident. Reading the code sequentially spends attention on categories automated tools already cover and exhausts it before reaching authorisation.
- How is secure code review different from a normal code review?
- A normal review asks whether the code does what it claims. A security review asks what else it does, and it treats absences as findings: the ownership check that was never written, the route registered without a guard, the query missing a tenant clause. It also looks beyond HTTP entry points to queue consumers, scheduled jobs, webhooks and internal endpoints that assume the network is trusted.
- What is mass assignment and why does it matter?
- Binding a request body directly onto a model or database entity, so any field the caller includes gets written. It matters because the object usually has fields the caller should never control, such as a role, an owner, a tenant identifier or a price. The vulnerability is invisible in a diff because the code is a single short line that looks like ordinary framework usage.
- Can code review find race conditions in business logic?
- Yes, and it is one of the few methods that can. Look for check-then-act sequences on anything representing value: reading a balance, deciding it is sufficient, then writing a decrement. Each statement is individually correct, which is why scanners stay silent. The fix is a database-level guarantee such as a conditional update, a row lock or a constraint, and its absence is the finding.
- When should a pull request get a security review?
- When the diff adds a route, consumer or job, changes who may call something, or introduces a new query, outbound request, file path, deserialiser or dependency. If none of those apply, security review adds nothing. Tying the trigger to constructs in the diff, ideally enforced by a code-owners rule on specific paths, produces review requests worth a reviewer's time instead of performative approvals.
- Is external code review worth paying for?
- For specific boundaries, yes: authentication flows, multi-tenant isolation, payment logic, permission models and anything cryptographic, where a reviewer who has seen many implementations spots failures quickly. It is poor value as a general sweep, because a reviewer without domain knowledge cannot distinguish an intentional design from a mistake. Scope it to named files and include a walkthrough with the engineer who wrote them.