06/11/2012

The Da Vinci Cod(e) Review

Introduction

This article is about doing Web Application security code review properly. Or at least the way I do it, which after enough pentests and code audits I'm fairly confident counts as properly. The best setup for a review is having the running application and the source sitting in front of you at the same time. You spot something suspicious in the code, you pop over to the browser and confirm it fires. No guessing, no theoretical findings that quietly die in triage six months later.

Ideally this lives inside a CI/CD pipeline. Your SAST tool flags something on a commit, you spin up the branch in a local or staging instance, and you check whether the finding is actually reachable before anyone burns a sprint on it. Tool flags it, human proves it. That loop is where the real security value sits. Everything else is theatre.


But first, what actually is a security code review? It's a systematic pass through source code aimed at finding and fixing the security mistakes that got missed during development. That's it. People run it in different shapes: pair programming, informal walkthroughs, formal inspections. Sometimes it's an internal security team, sometimes an outside contractor.

Bringing in an independent third party has real value beyond just extra eyes. Someone who joined at the tail end of development has no "emotional attachment to the code". They didn't fight for that architecture decision in a meeting six months ago. That distance is worth paying for.

In a modern DevSecOps setup, code review isn't a single event anymore. PR reviews, SAST scans firing on every commit, and periodic deep-dive manual audits all run at once. The real shift over the last few years is treating this as a living process, not a gate you pass through once and forget.

Types of code review

Code review practices generally split into three buckets: pair programming, formal review, and lightweight review. Formal review, the Fagan inspection style, is the heavyweight option: multiple participants, multiple phases, developers sitting round a table walking through printed code line by line. It's thorough, and there's decades of data showing formal inspections catch defects other methods miss. Lightweight review needs far less overhead and, done properly, can be just as effective.

Lightweight reviews are often conducted as part of the normal development process:
  1. Over-the-shoulder – One developer looks over the author's shoulder as the latter walks through the code.
  2. Email pass-around – Source code management system emails code to reviewers automatically after checkin is made.
  3. Pair Programming – Two authors develop code together at the same workstation, such is common in Extreme Programming.
  4. Tool-assisted code review – Authors and reviewers use specialized tools designed for peer code review (e.g. GitHub Pull Requests, GitLab Merge Requests, Gerrit).
A fourth category has emerged: AI-assisted code review. Tools like GitHub Copilot, Semgrep/Opengrep's assistant, and Snyk Code now give real-time security feedback straight in the IDE or the PR. As of mid-2026, Copilot's code review can pull in agent skills and MCP servers, so it checks your PR against your own team's internal standards and tooling instead of a generic ruleset. That's a genuine capability jump. It's also, as I'll get to further down, a new place for things to go wrong. Same rule as ever: human verification is not optional.

Important note: Tools can be used to perform this task but they always need human verification. Tools do not understand context, which is the keystone of security code review. Tools are good at assessing large amounts of code and pointing out possible issues, but a person needs to verify every single result to determine if it is a real issue, if it is actually exploitable, and calculate the risk to the enterprise. This matters more now that so much of the code hitting your PRs was written by an LLM. Veracode's Spring 2026 GenAI code security study tested over 100 models and found 45% of AI-generated samples introduced an OWASP Top 10 vulnerability. AppSec Santa's 2026 run came out lower at 25.7% across 522 samples, still not a number you want anywhere near a production deploy. CodeRabbit's research put AI-written code at 1.88x more likely to carry a vulnerability than the human-written equivalent. And the velocity problem compounds it: AI-assisted developers ship commits three to four times faster than their peers but introduce security findings at roughly ten times the rate. Your SAST tool will flag a chunk of that. Only a human reviewer, one who understands the business context, decides whether the flagged pattern is actually exploitable or just noise.

What is the most important thing in a code review

The single most important thing you bring to a code review is a threat model. Threat modeling is a structured way of analysing an application's security: what you're protecting, who's coming after it, and how. It isn't a review technique on its own. It's the lens you review through, and doing it early in the SDLC means security gets built in from day one instead of bolted on afterwards. It also hands the reviewer a map: where the entry points sit, and what's actually at risk behind each one.

In 2020, a group of threat modeling practitioners, researchers and authors published the Threat Modeling Manifesto, a document that distills the collective knowledge of the community into values and principles, similar to the Agile Manifesto. The Manifesto anchors all threat modeling around four fundamental questions:
  1. What are we building? Understand the system through diagrams (DFDs, architecture diagrams).
  2. What can go wrong? Identify threats using frameworks like STRIDE, PASTA, LINDDUN, or Attack Trees.
  3. What are we going to do about it? Define countermeasures and mitigations.
  4. Did we do a good enough job? Validate and iterate on the threat model.
These four questions are your north star, whichever specific framework you end up reaching for. The mindset shift underneath all of it: look at the system the way an attacker would, not the way you built it. Threat modeling isn't a security-team-only exercise anymore either. It needs developers, architects, business analysts, DevOps, and security in the same room, or at least the same thread.

Threat Modeling Frameworks: A Modern Overview

There are several established frameworks. Which one you reach for depends on your organisation's maturity, what kind of system you're analysing, and whether you're chasing security risk, privacy risk, or business risk. Here's where things actually stand in 2026.

STRIDE

STRIDE is the most widely used framework, developed originally at Microsoft. It splits threats into six categories:
  • Spoofing: Can an attacker impersonate another user or system?
  • Tampering: Can an attacker modify data in transit or at rest?
  • Repudiation: Can an attacker deny having performed an action?
  • Information Disclosure: Can sensitive data leak to unauthorized parties?
  • Denial of Service: Can an attacker degrade or disrupt service availability?
  • Elevation of Privilege: Can an attacker gain unauthorized access to higher-level functions?
STRIDE suits teams new to threat modeling. It's quick to teach and quick to adopt, and it slots into agile sprints without much friction. In practice: draw a Data Flow Diagram of your system, then for every element ask "can this be spoofed? Tampered with?" and work down the six. Microsoft built STRIDE into their Security Development Lifecycle and credits it as one of the reasons their products got measurably more secure over the years that followed.

Limitation: STRIDE only covers security threats. It says nothing about privacy, doesn't connect to business risk, and doesn't account for attacker motivation. It's also static, built for design time, and won't adapt on its own as new threat intelligence comes in.

PASTA (Process for Attack Simulation and Threat Analysis)

PASTA is a seven-stage, risk-centric methodology from Tony UcedaVélez and Marco M. Morana. Where STRIDE sorts threats into categories, PASTA ties technical risk directly to business impact, which is why it shows up more often in regulated enterprises where every security decision needs a business justification attached.

The seven stages of PASTA are:
  1. Define Objectives: Identify business objectives, security requirements, compliance requirements, and data classification for the application in scope.
  2. Define Technical Scope: Map all system components, their relationships, interdependencies, and the attack surface.
  3. Application Decomposition: Break down the system into data flows, processes, trust boundaries, user roles, and permissions.
  4. Threat Analysis: Identify threat actors, their motivations, and create Attack Trees to model how they could achieve their goals.
  5. Vulnerability Analysis: Correlate threats with known vulnerabilities using data from vulnerability scanners, penetration test reports, and threat intelligence feeds.
  6. Attack Modeling: Simulate attack scenarios to test the viability of identified threats against existing countermeasures.
  7. Risk and Impact Analysis: Calculate residual risk, prioritize findings by business impact, and define remediation strategies.
PASTA fits mature organisations that want their security work tied to business risk, not floating on its own. It's the right depth for high-assurance systems: finance, healthcare, critical infrastructure. Its real edge over STRIDE is that it forces the room to be cross-functional: developers, architects, business analysts, risk professionals, and SOC team members all in the process together, not just the dev team ticking a security box.

Limitation: PASTA takes more expertise to run and a lot more time. It lives or dies on how good your data about the system and its architecture actually is. Don't treat it as a quick start. Budget for stakeholder coordination and expect weeks, not an afternoon.

Attack Trees

Attack Trees pair with any of the frameworks above. Bruce Schneier formalised them in 1999, building on earlier work by Edward Amoroso and the NSA, and they give you a visual, hierarchical map of how an attacker reaches a goal.

The structure is simple:
  • The root node represents the attacker's goal (e.g., "Steal user credentials").
  • Child nodes represent the different ways to achieve that goal.
  • Nodes are connected using AND/OR logic: OR nodes represent alternatives (any one path suffices), AND nodes represent steps that must all be completed.
  • Each node can carry additional metadata: likelihood, cost, required skill level, detectability.
For example, an Attack Tree for "Bypass Authentication" might look like:

Bypass Authentication [ROOT - OR]
├── Brute Force Password [OR]
│   ├── Online brute force (if no rate limiting)
│   └── Offline brute force (if password hashes leaked)
├── Credential Stuffing [OR]
│   └── Use credentials from previous breaches
├── Session Hijacking [OR]
│   ├── Steal session cookie via XSS
│   └── Session fixation attack
├── Exploit Password Reset [OR]
│   ├── Predictable reset tokens
│   └── Account takeover via email compromise
└── SQL Injection on Login [OR]
    └── Bypass authentication via tautology (e.g. ' OR '1'='1)

Attack Trees earn their keep past the diagram itself: they hand you tactical, targeted defenses instead of a vague "we should improve auth." They also work well in front of leadership. Non-technical stakeholders can follow a tree even if they can't follow a CVSS score. Inside PASTA specifically, you build Attack Trees during stage four, Threat Analysis, to model how each identified threat actor gets to their goal.

Tip: Tools like OWASP Threat Dragon, Microsoft Threat Modeling Tool, IriusRisk, and Devici can help you build and maintain Attack Trees as living documents that evolve with your application.

LINDDUN (Privacy Threat Modeling)

LINDDUN comes out of KU Leuven and covers what STRIDE doesn't: privacy. STRIDE handles confidentiality, integrity, availability. LINDDUN handles the privacy-specific harms those categories were never built to catch. With GDPR, CCPA, and HIPAA enforcement only getting stricter, skipping privacy threat modeling on anything that touches personal data is a bet I wouldn't take.

LINDDUN stands for:
  • Linking: Can an adversary combine data to learn more about an individual?
  • Identifying: Can the identity of a data subject be determined?
  • Non-repudiation: Can a user be unable to deny an action (sometimes a privacy threat, not just a security feature)?
  • Detecting: Can an adversary detect that a user is using a system?
  • Data Disclosure: Can personal data leak to unauthorized parties?
  • Unawareness: Are users insufficiently informed about data collection and processing?
  • Non-compliance: Does the system fail to comply with privacy regulations and best practices?
LINDDUN runs on the same four Manifesto questions and can sit on the same DFD as STRIDE, so you're not duplicating the diagramming work to cover both security and privacy. It comes in a few flavours: LINDDUN GO (a card-based, gamified version for a fast brainstorm session), LINDDUN PRO (a systematic, exhaustive approach starting straight from the DFD), and LINDDUN MAESTRO (the advanced approach for enriched system descriptions).

When to use it: If your application processes personal data (user profiles, health records, financial information, location data), run LINDDUN alongside STRIDE. Regulatory fines plus the reputational hit from a privacy incident now regularly outstrip what a plain security breach costs you.

Choosing Your Framework

You don't have to marry one framework. Most teams start with STRIDE for general coverage, layer in LINDDUN once personal data enters the picture, and bring in PASTA later once the organisation wants threats tied to business objectives instead of just a list of findings. Attack Trees slot into any of them when you need to drill into one specific scenario. The programs that actually work evolve this way: lightweight first, cross-functional later. Nobody builds PASTA on day one.

The Threat Modeling Process

Regardless of which framework you choose, the threat modeling process can be decomposed into 3 high level steps:

Step 1: Decompose the Application:
  • Create use-cases to understand how the application is used.
  • Identify entry points (APIs, web forms, file uploads, message queues, webhooks).
  • Identify assets (databases, secrets, PII, session tokens, cryptographic keys).
  • Identify trust levels and trust boundaries between external entities.
  • Map data flows using DFDs or sequence diagrams.
Note: This stage is about understanding the context of the Web Application and everything touching it. In a modern architecture, that means microservices communication, API gateways, third-party integrations, cloud provider boundaries, and the container orchestration layer sitting under all of it.

The following images show the Business Architecture (Business Owner's Perspective) and Business Architecture Behavior of a Web Application:


Note: Lists the entities important to the business. Business entities can be a person, a thing or a concept that is part of or interacts with the business process (Proforma 2003). In the example of "XYZ-Match", the business entities include the following: Investors, Entrepreneurs, "XYZ-Match" web system.


Note: Lists the processes in which the business operates. In the example of "XYZ-Match", "Investor listing information to Venture Capital Directory" is one of such business processes.

Step 2: Determine and rank threats using your chosen categorization methodology:
  • Authentication and Identity Management
  • Authorization and Access Control
  • Session Management
  • Input Validation and Output Encoding
  • Data Protection in Storage and Transit (encryption at rest, TLS, key management)
  • Auditing, Logging, and Monitoring
  • Configuration Management and Secrets Management
  • Error Handling and Exception Management
  • Supply Chain and Dependency Security
Note: This stage is about mapping vulnerabilities to a category. Threat listing is an important part of a Web Application code audit. Threat lists based on the STRIDE model are useful for identifying threats against attacker goals. Grouping the Web Application threats this way tells you where the actual weak points cluster. If half your findings land under Authentication, that's your blinking red light: not six unrelated bugs, one systemic gap wearing six different masks.

Step 3: Determine countermeasures and mitigation.

Note: Such countermeasures can be identified using threat-countermeasure mapping lists. The risk mitigation strategy might involve evaluating these threats from the business impact that they pose and reducing the risk.

The objective of risk management should be to reduce the impact that the exploitation of a threat can have to the application (not to necessarily mitigate the risk!). This can be done by responding to a threat with a risk mitigation strategy. In general there are five options to mitigate threats:
  1. Do nothing: for example, hoping for the best.
  2. Informing about the risk: for example, warning user population about the risk.
  3. Mitigate the risk: for example, by putting countermeasures in place.
  4. Accept the risk: for example, after evaluating the impact of the exploitation (business impact).
  5. Transfer the risk: for example, through contractual agreements and insurance.
The decision of which strategy is most appropriate depends on the impact an exploitation of a threat can have, the likelihood of its occurrence, and the costs for transferring or avoiding it.

  • Define the application requirements:
  1. Identify business objectives
  2. Identify user roles that will interact with the application
  3. Identify the data the application will manipulate
  4. Identify the use cases for operating on that data that the application will facilitate
  • Model the application architecture:
    • Model the components of the application
    • Model the service roles that the components will act under
    • Model any external dependencies (third-party APIs, open-source libraries, cloud services)
    • Model the calls from roles, to components and eventually to the data store for each use case
  • Identify any threats to the confidentiality, availability and integrity of the data and the application based on the data access control matrix that your application should be enforcing
  • Assign risk values and determine the risk responses
  • Determine the countermeasures to implement based on your chosen risk responses
  • Continually update the threat model based on the emerging security landscape. Threat modeling is not a one-time activity, it must evolve as the application, its dependencies, and the threat landscape change.

Modern Tools for Security Code Auditing

The tooling has moved a long way past grep. Graudit, RATS, and findstr, the old signature scanners, still have some educational value if you want to understand pattern matching at its most basic. But production SAST today does semantic analysis, not string matching. Here's where the field actually sits.

Semgrep / Opengrep

Semgrep is a lightweight static analysis tool built on semantic pattern matching rather than plain text matching. It understands code structure, so it stays fast without falling back to regex guesswork, and you write custom rules in YAML that look almost like the code you're hunting for.

semgrep scan --config=auto /path/to/code

It's fast, needs no compilation step, covers 20+ languages, and drops straight into CI/CD, IDE plugins, and PR checks. The commercial Semgrep AppSec Platform layers on cross-file/cross-function dataflow analysis, SCA, secrets detection, and an AI-powered assistant for triage and autofix.

Note: Here's the part that's actually changed since I last touched this piece. In January 2025, Semgrep shifted its open-source licensing, and ten-plus rival AppSec vendors (Aikido, Endor Labs, Arnica, Kodem, Legit, Mobb, Orca Security, Phoenix Security and others) forked the community edition into Opengrep under LGPL-2.1. By mid-2026 that fork is not a scrappy side project: 189+ contributors, 9,500+ commits, a full-time OCaml team on the core engine, and releases shipping on a roughly weekly cadence (v1.20 added Python structural pattern matching in April, v1.21 exposed taint-intrafile over LSP for IDE integrations in May). It restores taint analysis, inter-procedural scanning, and Windows support, all of which the Semgrep Community Edition had dropped, and it kept the original rule format and SARIF output, so swapping the binary into an existing pipeline is close to a non-event. If you're choosing between the two today, Opengrep is the one with no licensing string attached.

CodeQL (GitHub Advanced Security)

CodeQL compiles your source into a queryable relational database: the AST, the data flow graph, the control flow graph. You write queries in QL, a Datalog-derived declarative language, to traverse it. It's the deep-end tool. CodeQL variant analysis has turned up more than 400 CVEs in open-source projects to date, and it stays free for open-source repos.

codeql database create mydb --language=java --source-root=/path/to/code
codeql database analyze mydb codeql/java-queries:codeql-suites/java-security-and-quality.qls --format=sarif-latest --output=results.sarif

Copilot Autofix now sits on top of CodeQL results and suggests AI-generated fixes directly in the PR. Genuinely useful when the fix is mechanical, genuinely dangerous when it isn't and nobody checks.

Limitation: QL has a real learning curve, and building the database means compiling your code first, so it's slower than Semgrep for a quick pass. Treat this as a research and deep-audit tool, not something you bolt onto a fast-moving DevSecOps pipeline as your only gate.

Other Notable Tools

  • Snyk Code: Developer-focused SAST with real-time IDE scanning and AI-trained detection engine. Strong on AI-generated code pattern detection.
  • SonarQube: Combines SAST with code quality checks. Good for teams that want security and maintainability in one platform.
  • Checkmarx / Veracode / OpenText (Fortify): Enterprise-grade SAST platforms with deep scanning, compliance reporting, and legacy language support.
  • Bandit: Open-source Python-specific SAST tool. Lightweight and great for Python-heavy shops.
  • OWASP Dependency-Check / Trivy: SCA tools for scanning third-party dependencies for known vulnerabilities.
  • GitGuardian / Gitleaks: Secrets detection tools that scan repos for exposed credentials, API keys, and tokens.

Important: No single tool covers everything, and anyone selling you one that does is selling. A real AppSec stack layers SAST, SCA, secrets detection, DAST, and IaC scanning, each one catching what the others miss. And the golden rule hasn't moved an inch: a tool is only as good as the human reviewing its output.

The new attack surface: AI agents in the review loop

There's a wrinkle in all of this that didn't exist when I first wrote this piece: the reviewer itself might now be an agent with tool access, not just a human with a linter.

GitHub made Copilot code review's agent skills and MCP server support generally available in July 2026, across Pro, Pro+, Business, and Enterprise. Your review agent can now pull in your team's internal tools and standards mid-review, which is a real capability jump. It also means your review agent has become a thing an attacker can target, not just a thing they hide from.

Case in point: in July 2026, researchers disclosed a flaw in Microsoft's own Azure DevOps MCP server where a single invisible comment on a pull request could hijack the reviewer's AI coding agent. The server tool returning PR descriptions didn't guard against prompt injection, so text a human reviewer would never even notice could redirect the agent's actions. And in April 2026, OX Security disclosed a systemic MCP architecture flaw across a supply chain touching over 150 million package downloads, roughly 200,000 vulnerable instances by their estimate. The original MCP spec shipped without mandatory authentication and assumed servers and their tools were inherently trustworthy. Attackers noticed that assumption immediately.

Run this through STRIDE for two minutes and it's obvious why this matters: an MCP-connected review agent is a new entry point with elevated privilege (it can comment, approve, sometimes merge) and a new spoofing surface (anything that looks like PR content but is actually an instruction). Threat model your review tooling the same way you threat model the application it's reviewing. If your AI code review agent has MCP tool access, that access itself needs a security review before it goes anywhere near production PRs. Don't skip that step because the tool is "just for code review." That's exactly the reasoning that got the Azure DevOps server compromised.

Cod(e) reviewing for SQL Injection

SQL Injection is still sitting in the OWASP Top 10 after multiple revisions, which after this many years should embarrass the industry more than it does. It persists because of one specific, boring mistake that keeps recurring: constructing SQL queries by concatenating untrusted input into a string.

Use parameterized queries (PreparedStatements in Java) instead of dynamic SQL statements. Validate all external input: ensure that all SQL statements recognize user inputs as variables, and that statements are precompiled before the actual inputs are substituted for the variables. Think about SQL injection defense in layers across the whole Web Application system. Input validation should occur at the Web Application input filter, the framework/ORM layer, and the database layer itself. Additional layers of defense can be added through a Web Application Firewall (WAF) and a Database Activity Monitor. Neither is a substitute for fixing the query.

The following picture shows a sequence of yes and no flow chart explaining an SQL injection flow:


Note: This is a simplified SQL Injection threat model. In practice, the decision tree branches further when you consider second-order injection, blind SQLi, and out-of-band channels.

The Vulnerable Code (Java: what NOT to do)

// VULNERABLE: SQL Injection via string concatenation
// This is the classic mistake: user input directly embedded in SQL

String username = request.getParameter("USER");       // From HTTP request, UNTRUSTED
String password = request.getParameter("PASSWORD");   // From HTTP request, UNTRUSTED

// DANGER: Direct concatenation of user input into SQL query
String sql = "SELECT User_id, Username FROM USERS WHERE Username = '"
    + username + "' AND Password = '" + password + "'";

Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

// An attacker entering: Username = ' OR '1'='1' --
// Produces: SELECT User_id, Username FROM USERS
//           WHERE Username = '' OR '1'='1' --' AND Password = ''
// Result: Authentication bypass, returns all users

When SQL statements are dynamically created as software executes, the input data can truncate, malform, or expand the query you meant to run. request.getParameter() here pulls straight from the HTTP request with zero validation: no length check, no character allowlist, nothing. That gap is exactly what lets an attacker submit SQL as the payload and rewrite what the statement actually does.

The Secure Code (Java: PreparedStatement / Parameterized Query)

// SECURE: Parameterized query using PreparedStatement
// The SQL structure is precompiled; user input is ALWAYS treated as data

String username = request.getParameter("USER");
String password = request.getParameter("PASSWORD");

// The '?' placeholders ensure input can never alter the query structure
String sql = "SELECT User_id, Username FROM USERS WHERE Username = ? AND Password = ?";

try (PreparedStatement pstmt = connection.prepareStatement(sql)) {

    pstmt.setString(1, username);  // Bound as data, not SQL code
    pstmt.setString(2, password);  // Bound as data, not SQL code

    try (ResultSet rs = pstmt.executeQuery()) {
        if (rs.next()) {
            int userId = rs.getInt("User_id");
            String loggedUser = rs.getString("Username");
            // Authentication successful
        } else {
            // Authentication failed
        }
    }
} catch (SQLException e) {
    logger.error("Database error during authentication", e);
    // NEVER expose stack traces or SQL errors to the user
}

The PreparedStatement precompiles the query with placeholder markers (?). Call setString() and the JDBC driver treats the input strictly as a string literal, full stop. It can never be reinterpreted as SQL code. Feed it ' OR '1'='1 and the database goes looking for a username that is literally that string. It finds nothing, because nobody's username is ' OR '1'='1.

Modern Alternative: Using an ORM (JPA/Hibernate)

In modern Java applications, you often interact with the database through an ORM rather than raw JDBC. Here is how the same query looks using JPA (Java Persistence API):

// SECURE: JPA Named Query, parameterized by default

@Entity
@NamedQuery(
    name = "User.findByCredentials",
    query = "SELECT u FROM User u WHERE u.username = :username AND u.password = :password"
)
public class User { ... }

// Usage:
TypedQuery<User> query = entityManager.createNamedQuery("User.findByCredentials", User.class);
query.setParameter("username", request.getParameter("USER"));
query.setParameter("password", request.getParameter("PASSWORD"));
List<User> results = query.getResultList();

Warning: ORMs don't make you safe by default. Build your JPQL or HQL with string concatenation and you've recreated the exact same hole in a fancier wrapper. The rule doesn't change with the abstraction layer: always use parameterized queries.

Python Example (for comparison)

# VULNERABLE: string formatting with untrusted input
cursor.execute(f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'")

# SECURE: parameterized query
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

What to grep for during code review (SQL Injection indicators)

When performing a manual code review or writing custom SAST rules, look for these patterns:
  • Statement.execute( or Statement.executeQuery( combined with string concatenation (+)
  • "SELECT ... " + variable or "INSERT ... " + variable or "UPDATE ... " + variable
  • String.format() used to build SQL queries
  • f"SELECT ..." (Python f-strings in SQL context)
  • cursor.execute("... %s ..." % variable) (Python old-style string formatting: not the same as a parameterized %s placeholder)
  • $"SELECT ..." (C# string interpolation in SQL context)
A Semgrep rule to detect Java SQL injection looks like this:

# semgrep-rule: java-sql-injection.yaml
rules:
  - id: java-sqli-string-concat
    patterns:
      - pattern: |
          String $QUERY = "..." + $INPUT + "...";
          ...
          $STMT.executeQuery($QUERY);
    message: >
      Potential SQL injection: user input concatenated into SQL query.
      Use PreparedStatement with parameterized queries instead.
    severity: ERROR
    languages: [java]

Epilogue

The actual goal of a security code review is teaching developers to write secure code in the first place. Everything else, the finding count, the severity ratings, the compliance checkbox, is secondary to that. Give developers a controlled set of rules to measure their own code against and you get better code next sprint, not just a patched bug this one. Semgrep, Opengrep, CodeQL, all of them work best when they deliver that feedback immediately: in the IDE, in the PR, while the context is still fresh in the developer's head.

The landscape has moved a long way since grep-based scanning. We've got AI-assisted remediation, cross-function taint analysis, reachability-based SCA, privacy-specific threat modeling, and now AI agents sitting inside the review pipeline itself with their own attack surface. None of that changes the fundamental truth: tools augment judgment, they don't replace it. The best reviews I've done, and the ones I've watched other people do well, always came down to a reviewer who knew the right tool for the job, checked every finding against real context, and could translate the risk into terms that got a business to actually act on it.

Threat model your applications. STRIDE for security, LINDDUN for privacy, PASTA when you need threats tied to business impact, Attack Trees when you need to drill into one specific scenario. Threat model your AI tooling too. It has entry points and trust boundaries just like everything else you're reviewing. Embed SAST in your pipeline. Review every finding with human eyes. And never, ever concatenate user input into a SQL query. Fifteen years into this industry and I'm still writing that sentence, which tells you everything about how well we're actually doing.

References:

The CVE Explosion Nobody Budgeted For

The CVE Explosion Nobody Budgeted For 72,000 vulnerabilities a year, a funding scare at the program's core, and an exploit window t...