Security-Focused Code Review
Learn how to conduct security-focused code reviews covering OWASP Top 10 vulnerabilities, secure coding patterns, SAST tools, and AI-powered review.
20 min read
Why Security Review Is Different from General Review
Most code reviews focus on correctness, readability, and maintainability. A reviewer reads a pull request, checks that the logic makes sense, confirms naming conventions are followed, and approves. Security review demands a fundamentally different mindset.
When you review code for security, you stop asking “does this work?” and start asking “how can this be abused?” The mental shift is from constructive to adversarial. You need to think like an attacker examining every input boundary, every trust assumption, and every data flow for potential exploitation.
This distinction matters because most developers are not trained to think adversarially. A 2025 survey by Secure Code Warrior found that only 14% of developers felt confident identifying security vulnerabilities during code review. The remaining 86% either missed vulnerabilities entirely or flagged false positives because they lacked the framework to distinguish real risks from benign patterns.
Security review is also asymmetric. A general reviewer can miss a naming inconsistency, and the worst outcome is slightly less readable code. A security reviewer who misses an SQL injection opens a path to a data breach that costs millions. The stakes demand more rigor, more structure, and better tooling.
Three principles separate effective security review from general review:
Assume all input is hostile. Every value that originates from outside the application (HTTP parameters, headers, file uploads, database results from shared tables, environment variables set by orchestration) should be treated as potentially attacker-controlled until validated.
Review the boundaries, not just the logic. Security vulnerabilities cluster at boundaries: where user input enters the system, where data moves between trust zones, where privileges are checked (or not checked), and where data leaves the system (logs, APIs, rendered HTML). Focus your attention there.
Verify the absence of controls, not just the presence of code. The most dangerous security bugs are things that are missing, like a forgotten authorization check, a skipped input validation step, or an absent encryption call. General code review rarely catches missing code because reviewers focus on what is present in the diff.
The OWASP Top 10 Through a Reviewer’s Lens
The OWASP Top 10 is the most widely recognized framework for categorizing web application security risks. As a code reviewer, you do not need to memorize every CWE number, but you need a working understanding of each category and what vulnerable code looks like in practice.
Here is a condensed reviewer’s guide to each category, ordered by how commonly each appears in code review:
A03: Injection. The most reviewable category. SQL injection, command injection, and cross-site scripting (XSS) are pattern-matchable and appear frequently in pull requests. We cover these in detail in the next section.
A01: Broken Access Control. The most dangerous category. Missing or incorrect authorization checks are logic-level issues that automated tools struggle to catch. During review, ask: “Can a user reach this endpoint or data without the correct permissions?”
A02: Cryptographic Failures. Look for hardcoded keys, weak algorithms (MD5, SHA1 for passwords), missing encryption for sensitive data at rest or in transit, and improper certificate validation.
A04: Insecure Design. This is about flawed architecture rather than flawed implementation. Rate limiting, account lockout, and transaction validation are design-level controls that should exist before code is written.
A05: Security Misconfiguration. Default credentials, overly permissive CORS policies, verbose error messages in production, and unnecessary features left enabled. These often appear in configuration files that slip through review.
A06: Vulnerable and Outdated Components. Dependency updates introducing known CVEs. This is better caught by SCA (Software Composition Analysis) tools than manual review, but reviewers should flag suspicious dependency additions.
A07: Identification and Authentication Failures. Weak password policies, missing multi-factor authentication, session fixation, and predictable session tokens.
A08: Software and Data Integrity Failures. Deserializing untrusted data, missing integrity checks on updates, and insecure CI/CD pipeline configurations.
A09: Security Logging and Monitoring Failures. Missing audit logs for sensitive operations, logging sensitive data (passwords, tokens), and insufficient alerting.
A10: Server-Side Request Forgery (SSRF). Applications fetching URLs provided by users without validation, allowing attackers to reach internal services.
Injection Attacks
Injection vulnerabilities remain the most common security issues caught during code review. They share a single root cause: untrusted data is concatenated into a command or query that an interpreter executes.
SQL Injection
SQL injection occurs when user input is interpolated directly into a SQL query string. Here is the classic vulnerable pattern:
# VULNERABLE - user input directly in query string
def get_user(username):
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
return cursor.fetchone()
An attacker passing ' OR '1'='1' -- as the username retrieves all users. The fix is always the same: use parameterized queries.
# SECURE - parameterized query
def get_user(username):
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))
return cursor.fetchone()
During review, look for string concatenation or f-string interpolation anywhere near database calls. This includes ORMs. Raw query methods in Django (raw(), extra()) and SQLAlchemy (text()) are common injection points even in codebases that otherwise use safe ORM patterns.
Command Injection
Command injection is the system-level equivalent of SQL injection. It occurs when user input is passed to shell commands.
# VULNERABLE - user input in shell command
import os
def ping_host(hostname):
os.system(f"ping -c 4 {hostname}")
An attacker supplying google.com; rm -rf / as the hostname executes an arbitrary command. The secure approach avoids shell interpretation entirely:
# SECURE - subprocess with argument list, no shell
import subprocess
def ping_host(hostname):
# Validate hostname format first
if not re.match(r'^[a-zA-Z0-9.-]+$', hostname):
raise ValueError("Invalid hostname")
subprocess.run(["ping", "-c", "4", hostname], check=True)
During review, flag any use of os.system(), subprocess.run(shell=True), child_process.exec() in Node.js, or Runtime.exec() in Java where user input reaches the command string.
Cross-Site Scripting (XSS)
XSS occurs when user-controlled data is rendered in HTML without proper encoding.
// VULNERABLE - innerHTML with user data
function displayComment(comment) {
document.getElementById('output').innerHTML = comment.text;
}
// SECURE - textContent escapes HTML automatically
function displayComment(comment) {
document.getElementById('output').textContent = comment.text;
}
In server-rendered applications, look for raw HTML rendering that bypasses the template engine’s auto-escaping:
# VULNERABLE - Django's mark_safe with user input
from django.utils.safestring import mark_safe
def render_bio(request):
bio = request.GET.get('bio', '')
return render(request, 'profile.html', {'bio': mark_safe(bio)})
Modern frameworks like React, Vue, and Angular auto-escape by default, but dangerouslySetInnerHTML (React), v-html (Vue), and [innerHTML] (Angular) bypass this protection. Flag these in every review.
Authentication and Access Control Flaws
Authentication and authorization bugs are among the highest-impact vulnerabilities because they grant attackers access to entire accounts or administrative functions. Unlike injection, these are often logic-level flaws that require human judgment to detect.
Missing authorization checks. The most common access control flaw is simply forgetting to verify that the requesting user has permission to perform an action.
# VULNERABLE - no authorization check
@app.route('/api/users/<user_id>/profile', methods=['PUT'])
def update_profile(user_id):
data = request.get_json()
user = User.query.get(user_id)
user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
# SECURE - verify the requesting user owns this profile
@app.route('/api/users/<user_id>/profile', methods=['PUT'])
@login_required
def update_profile(user_id):
if current_user.id != int(user_id) and not current_user.is_admin:
abort(403)
data = request.get_json()
user = User.query.get_or_404(user_id)
user.email = data['email']
db.session.commit()
return jsonify(user.to_dict())
During review, check every endpoint that modifies data or returns sensitive information. Ask: “What prevents User A from calling this endpoint with User B’s ID?” This is called an IDOR (Insecure Direct Object Reference) vulnerability, and it is consistently in the top findings of bug bounty programs.
Broken authentication flows. Review password reset flows, magic link implementations, and OAuth callbacks carefully. Common issues include:
- Password reset tokens that do not expire
- Token generation using predictable values (timestamps, sequential IDs)
- Missing rate limiting on login attempts
- Session tokens that are not invalidated on password change
Privilege escalation. Look for role checks that use client-supplied values rather than server-side session data:
// VULNERABLE - role from request body
app.post('/api/admin/users', (req, res) => {
if (req.body.role === 'admin') {
// perform admin action
}
});
// SECURE - role from authenticated session
app.post('/api/admin/users', (req, res) => {
if (req.session.user.role === 'admin') {
// perform admin action
}
});
Sensitive Data Exposure
Sensitive data exposure during code review falls into two major categories: secrets committed to source control, and personal or financial data leaking through logs, error messages, or APIs.
Secrets in Code
Hardcoded credentials are one of the easiest vulnerabilities to detect during review, yet they persist at a staggering rate. GitGuardian’s 2025 State of Secrets Sprawl report found over 12 million new secrets exposed in public GitHub repositories in a single year.
Watch for these patterns:
# VULNERABLE - hardcoded secrets
API_KEY = "sk-proj-abc123def456ghi789"
DB_PASSWORD = "production_p@ssw0rd!"
JWT_SECRET = "my-secret-key"
# SECURE - environment variables
import os
API_KEY = os.environ["API_KEY"]
DB_PASSWORD = os.environ["DB_PASSWORD"]
JWT_SECRET = os.environ["JWT_SECRET"]
Beyond obvious API keys, look for connection strings with embedded passwords, private keys committed alongside code, and .env files that are not in .gitignore.
Logging PII
Logging is a frequent source of data exposure. Developers add logging for debugging and forget to remove it, or they log entire request objects that contain sensitive fields.
// VULNERABLE - logging sensitive data
logger.info("User login attempt: " + request.toString());
// Logs: "User login attempt: {username=john, password=s3cret, ...}"
// SECURE - log only what is needed
logger.info("User login attempt for: {}", request.getUsername());
During review, check that log statements never include passwords, API keys, session tokens, credit card numbers, Social Security numbers, or any data subject to GDPR, HIPAA, or PCI-DSS. Also verify that error responses sent to clients do not include stack traces, internal paths, or database details.
Security Review Checklist
Use this checklist during security-focused reviews. Not every item applies to every PR, but scanning through it ensures you do not miss common vulnerability classes.
Input validation:
- All user input is validated on the server side (client-side validation is a UX feature, not a security control)
- Input validation uses allow-lists rather than deny-lists where possible
- File uploads validate file type, size, and content (not just the extension)
Injection prevention:
- Database queries use parameterized statements or ORM methods
- No string concatenation in SQL, shell commands, LDAP queries, or XML
- Template rendering uses auto-escaping; any bypass (
dangerouslySetInnerHTML,mark_safe,v-html) is justified and the input is sanitized
Authentication and authorization:
- Every endpoint that modifies data or returns sensitive data has an authorization check
- Authorization uses server-side session data, not client-supplied values
- Password storage uses bcrypt, scrypt, or Argon2 (never MD5 or plain SHA)
- Session tokens are regenerated after login and invalidated on logout
Data protection:
- No hardcoded secrets, API keys, or passwords
- Sensitive data is encrypted at rest and in transit
- Log statements do not contain passwords, tokens, PII, or financial data
- Error responses do not expose internal details (stack traces, SQL errors, file paths)
Dependencies:
- New dependencies are from reputable sources with active maintenance
- No known CVEs in added or updated dependencies
- Lock files (
package-lock.json,Pipfile.lock) are committed and reviewed
Configuration:
- CORS policies are scoped to specific origins, not
* - Security headers (CSP, HSTS, X-Frame-Options) are configured
- Debug mode and verbose error pages are disabled in production configurations
SAST Tools for Automated Security Review
Manual security review does not scale. A team producing 50 pull requests per week cannot have a security engineer manually review each one. This is where Static Application Security Testing (SAST) tools provide essential coverage. For a comprehensive comparison, see our guide to the best SAST tools.
Semgrep
Semgrep is a fast, open-source static analysis tool that matches code patterns using a syntax close to the target language. Its security rule library includes over 2,800 community rules covering OWASP Top 10 patterns. Semgrep scans a typical repository in 10-30 seconds, making it practical as a CI check on every pull request.
What makes Semgrep particularly effective for security review is its custom rule authoring. If your team identifies a vulnerability pattern specific to your codebase (say, a custom database wrapper that bypasses parameterization), you can write a Semgrep rule in minutes that catches it in all future PRs. Semgrep’s paid tier adds Semgrep Assistant, which uses AI to triage findings and reduce false positives.
Snyk Code
Snyk Code uses a hybrid approach: AI-powered semantic analysis layered on top of traditional pattern matching. Its strongest differentiator is cross-file dataflow analysis. While most SAST tools analyze files in isolation, Snyk Code traces data flow across function calls, modules, and even microservice boundaries to detect vulnerabilities like SQL injection where the source and sink are in different files.
Snyk Code integrates directly into IDEs (VS Code, IntelliJ) and provides real-time feedback as developers write code, catching vulnerabilities before they even reach a pull request.
Checkmarx
Checkmarx is the enterprise standard for comprehensive SAST. It supports over 30 languages, has the deepest rule library in the industry, and provides compliance-oriented reporting for standards like PCI-DSS, SOC 2, and HIPAA. Checkmarx is typically deployed in regulated industries like financial services, healthcare, and government, where audit trails and compliance documentation are mandatory.
The trade-off is cost and complexity. Checkmarx requires significant configuration and tuning to reduce false positive rates, and pricing starts at approximately $40,000 per year.
SonarQube
SonarQube provides a broad code quality and security platform. Its security rules cover OWASP Top 10 and CWE Top 25, and its “Security Hotspots” feature flags code that requires human review without marking it as a definitive vulnerability. This is useful for patterns that are context-dependent, because a hardcoded string might be a secret or might be a test fixture.
SonarQube’s Community Build is free and self-hosted, making it accessible to teams of any size. The Developer and Enterprise editions add branch analysis, PR decoration, and more language support.
AI-Powered Security Review
The emergence of LLM-powered code review tools in 2025-2026 has introduced a new layer to security review. Unlike rule-based SAST tools, AI reviewers can understand context, reason about business logic, and catch vulnerability patterns that no one has written a rule for. For a deeper dive, read our analysis of AI code review for security.
AI-powered security review excels in three areas where traditional SAST falls short:
Logic-level vulnerabilities. An AI reviewer can recognize that a payment endpoint processes refunds without verifying that the requesting user is the original purchaser. No SAST rule covers this because the vulnerability is specific to the application’s business logic.
Missing controls. AI can identify the absence of security controls. It can look at a new API endpoint and flag that it lacks rate limiting, audit logging, or input validation. These are things that are missing from the diff rather than present in it.
Contextual risk assessment. AI can assess the severity of a finding based on context. A SQL query built with string concatenation in a test file is low risk. The same pattern in a production API handler exposed to the internet is critical. SAST tools flag both equally; AI tools can differentiate.
Tools like CodeRabbit, Snyk Code with AI triage, and Aikido are leading this space. However, AI security review has important limitations: it is non-deterministic (the same code may get different findings on successive scans), it can hallucinate vulnerabilities that do not exist, and it should never be the sole security layer.
The most effective approach combines AI review for contextual analysis with deterministic SAST for exhaustive pattern coverage. Use Semgrep to catch every SQL injection pattern reliably, and use AI to catch the business logic flaw that no rule covers.
Building a Security Review Culture
Tools alone do not create secure software. The most sophisticated SAST pipeline is worthless if developers dismiss every finding as a false positive or if security review is seen as an obstacle to shipping.
Building a security review culture requires investment in three areas:
Security training for all developers. Every engineer who writes code should understand the OWASP Top 10 at a working level. This does not mean formal certification. A four-hour workshop covering injection, access control, and data exposure, followed by hands-on exercises with intentionally vulnerable applications (like OWASP WebGoat or Juice Shop), gives developers the vocabulary and instincts they need.
Security champions within teams. Designate one developer per team as the security champion. This person receives additional training, stays current on emerging vulnerability patterns, and serves as the first point of escalation for security questions during review. Security champions do not replace a dedicated security team, but they extend security awareness into every squad.
Blameless vulnerability tracking. When a security vulnerability is found (whether in review, by a scanner, or in production), treat it as a learning opportunity, not a disciplinary event. Track vulnerability trends to identify systemic issues (are injection bugs concentrated in one service? Is one team consistently missing authorization checks?) and address root causes through training and tooling, not blame.
Celebrate catches. When a reviewer catches a significant vulnerability during review, recognize it publicly. This reinforces that security review is valued, encourages thoroughness, and creates social incentives for the adversarial mindset that security review requires.
Shift-Left: Security in the Development Lifecycle
“Shift-left” means moving security practices earlier in the development lifecycle, from post-deployment scanning to pre-merge review to pre-commit analysis to design-time threat modeling. Each leftward shift reduces the cost of fixing a vulnerability by an order of magnitude.
Here is what a mature shift-left security practice looks like:
Design phase: Threat modeling. Before writing code, identify the security-relevant components of a feature. What data does it handle? What trust boundaries does it cross? What are the attack scenarios? Lightweight threat modeling using STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) takes 30 minutes and prevents entire classes of vulnerabilities.
Development phase: IDE-integrated scanning. Tools like Snyk Code and SonarQube IDE plugins provide real-time feedback as developers write code. A developer writing a SQL query with string concatenation sees a warning immediately, before the code is even committed. This is the fastest feedback loop possible.
Pre-commit: Secrets scanning. A pre-commit hook running a tool like gitleaks or trufflehog catches hardcoded secrets before they enter version control history. Once a secret is committed, even to a private repository, it should be considered compromised because Git history is permanent.
Pull request: SAST and AI review. This is where the security review checklist, SAST tools, and AI review converge. Configure Semgrep or SonarQube as required CI checks that block merges on high-severity findings. Layer AI review from tools like CodeRabbit or Aikido for contextual security analysis. Assign security-critical PRs (authentication, payment, data access) to your security champion for manual review.
Post-merge: Continuous monitoring. Even with pre-merge gates, some vulnerabilities will slip through. Continuous monitoring with tools like Checkmarx running scheduled full-repository scans catches vulnerabilities in code that was merged before the rule existed. Dependency scanning (SCA) monitors for newly disclosed CVEs in libraries you already use.
The goal is not to catch every vulnerability at every stage. Each layer catches a different class of issues, and the layers compound. A team with pre-commit secrets scanning, SAST on every PR, AI review for logic-level issues, and periodic full-repository scans will have dramatically fewer vulnerabilities reaching production than a team relying on any single approach.
Security code review is not a tax on productivity. When done well, with the right mindset, the right checklist, and the right tools, it is a core engineering practice that protects your users, your data, and your organization. The earlier you integrate it, the less it costs and the more effective it becomes. Start with the checklist in this chapter, add one SAST tool to your CI pipeline, and build from there. For detailed comparisons of the tools mentioned here, see our SAST tool reviews and AI code review security analysis.
Frequently Asked Questions
What are the most common security issues found in code review?
The most common issues include injection flaws (SQL, command, XSS), broken authentication and session management, sensitive data exposure (hardcoded secrets, unencrypted data), insecure deserialization, and missing input validation. The OWASP Top 10 provides a comprehensive framework for what to look for.
Should every code review include a security check?
Every review should include baseline security awareness by checking for obvious issues like hardcoded credentials, SQL injection, and XSS. For security-critical code (authentication, payment processing, data handling), conduct a dedicated security-focused review with a checklist.
Can SAST tools replace manual security code review?
SAST tools catch known vulnerability patterns efficiently but miss business logic flaws, complex attack chains, and context-dependent issues. Use SAST for automated baseline scanning and manual review for critical paths. AI-augmented SAST tools like Semgrep and Snyk Code are closing the gap but still need human oversight.
Continue Learning
Tool Reviews
Newsletter
Stay ahead with AI dev tools
Weekly insights, no spam.
Semgrep Review
Snyk Code Review
Checkmarx Review
SonarQube Review
Aikido Security Review