teachyou.ai academy
← All posts
Claude Code

Claude Code for Security Reviews: Finding Vulnerabilities

Ira Menon · Jun 20, 2026 · 14 min read

The 2 AM Pull Request Nobody Wanted To Review

Every engineering team has a version of this story. A junior developer ships a "quick fix" for a broken password reset flow the night before a release freeze. The pull request touches authentication code, gets a rubber-stamp approval from a tired reviewer, and merges. Three weeks later, someone discovers the reset endpoint accepts a user ID as a query parameter with no ownership check, letting anyone reset anyone else's password. That is not a hypothetical - it is a shape of bug that shows up constantly in real audits, usually described politely as an "insecure direct object reference" and less politely as "how did this get approved."

Security reviews are tedious, detail-obsessed work, which makes them exactly the kind of task humans are bad at doing consistently and AI coding assistants are increasingly good at supporting. Claude Code, Anthropic's command-line coding agent, has become a serious tool in this space - not as a replacement for a security engineer, but as a tireless first-pass reviewer that reads every line, follows every call chain, and never gets bored on file 400 of a 500-file diff. This article walks through how Claude Code actually finds vulnerabilities, what categories of bugs it catches reliably, where it struggles, and how to build a repeatable security review workflow around it.

Why Claude Code Is Different From a Linter

Traditional static analysis security testing (SAST) tools like Semgrep, CodeQL, or Snyk work by pattern-matching against known-bad code shapes. They are fast and deterministic, but they are also brittle - rename a variable in an unexpected way, wrap a tainted value in a helper function, or introduce a new framework, and the rule set often misses it entirely. SAST tools also produce notoriously high false-positive rates, which trains developers to ignore the output.

Claude Code approaches the problem differently because it reads code the way a human reviewer does: it understands intent, not just syntax. When you point it at a codebase and ask it to look for injection vulnerabilities, it is not scanning for a regex match on exec( - it is tracing where user input enters the system, following that value through every transformation, and reasoning about whether it ever reaches a dangerous sink without being sanitized. This is the same mental model a senior security engineer uses during a manual code review, and it is why Claude Code can catch vulnerabilities that pattern-based tools miss, particularly ones that span multiple files or require understanding business logic.

That said, Claude Code is not a SAST replacement - it is a complement. The best security workflows run both: deterministic scanners for known CVE patterns and dependency issues, and an LLM-based review for logic flaws, business-logic abuse, and anything that requires actually understanding what the code is supposed to do.

Setting Up a Real Security Review Workflow

A security review with Claude Code works best when it is structured, not improvised. Here is a workflow that holds up on real production codebases:

  1. Scope the review. Point Claude Code at a specific diff, pull request, or directory rather than an entire monorepo. Reviews degrade in quality when the context window is spent on irrelevant files.
  2. Give it the threat model. Tell it what the application does, who the users are, and what data is sensitive. A review of a healthcare app should weight PHI exposure differently than a review of an internal admin tool.
  3. Ask for OWASP-aligned categories explicitly. Don't just say "find bugs" - ask for injection flaws, broken access control, authentication weaknesses, sensitive data exposure, insecure deserialization, and SSRF, one category at a time or as an explicit checklist.
  4. Require file-and-line citations. Every finding should point to an exact file and line number, with a short explanation of the exploit path. This is non-negotiable - vague findings waste more time than they save.
  5. Ask for a proposed fix, not just a description. Claude Code can draft the patch; a human should still approve it before merge.
  6. Re-run after fixes land. Treat the review as iterative. Fixing one vulnerability sometimes introduces another (for example, adding input validation that breaks a legitimate use case, or fixing SQLi by string-escaping instead of parameterizing).

A prompt that works well in practice looks something like this:

You are doing a security review of the /api/payments directory.
This service handles Razorpay and Stripe webhook payloads and touches
customer billing records. Review for:
1. Injection (SQL, NoSQL, command, template)
2. Broken access control / IDOR
3. Authentication and session handling issues
4. Sensitive data exposure (secrets, PII, logs)
5. SSRF and insecure deserialization
6. Insecure cryptographic storage

For each finding, cite the exact file and line, explain the exploit
path an attacker would use, rate severity (Critical/High/Medium/Low),
and propose a minimal fix. Do not report style issues.

This kind of explicit, checklist-driven prompt consistently produces better results than an open-ended "review this for security issues," because it forces Claude Code to systematically walk through each OWASP category instead of stopping at the first obvious bug it notices.

Injection Flaws: Where Claude Code Earns Its Keep

SQL injection remains one of the most common vulnerabilities Claude Code catches in real reviews, and it is instructive to see why pattern matching alone often misses these cases. Consider this Node.js example:

// routes/orders.js
app.get('/api/orders', async (req, res) => {
  const { status, sortBy } = req.query;
  const query = `
    SELECT * FROM orders
    WHERE status = '${status}'
    ORDER BY ${sortBy}
  `;
  const result = await db.query(query);
  res.json(result.rows);
});

A naive scanner might flag the status interpolation but miss the sortBy interpolation entirely, because ORDER BY clauses cannot be parameterized with standard placeholders in most database drivers, so developers frequently "solve" this by string-concatenating a supposedly safe column name - except sortBy comes straight from req.query with zero allowlisting. Claude Code reliably flags both injection points and typically explains that sortBy is the more dangerous of the two because it is often overlooked, then proposes a fix using an allowlist of valid column names plus parameterized queries for the status value:

const ALLOWED_SORT_COLUMNS = new Set(['created_at', 'total', 'status']);

app.get('/api/orders', async (req, res) => {
  const { status, sortBy = 'created_at' } = req.query;

  if (!ALLOWED_SORT_COLUMNS.has(sortBy)) {
    return res.status(400).json({ error: 'Invalid sort column' });
  }

  const result = await db.query(
    `SELECT * FROM orders WHERE status = $1 ORDER BY ${sortBy}`,
    [status]
  );
  res.json(result.rows);
});

The same pattern extends to NoSQL injection in MongoDB-backed services, where a naive find({ email: req.body.email }) looks safe until someone realizes req.body.email can be an object like { "$ne": null }, turning an equality check into a query that matches every document. Claude Code catches this class of bug reliably because it reasons about the shape of untrusted input rather than just its presence, and it flags command injection in the same way - tracing any child_process.exec() or os.system() call back to see if user input reaches it unsanitized, and recommending execFile with an argument array instead of a shell string wherever it finds one.

Broken Access Control and the IDOR Problem

Insecure direct object references are, in practice, the single most common serious vulnerability found in custom web applications, and they are also the hardest category for traditional SAST tools to catch because the bug is not in a single dangerous function call - it is in the *absence* of a check. A pattern-matching tool has nothing to match against when the vulnerability is a missing if statement.

This is where an LLM-based review genuinely shines. Claude Code traces the full request lifecycle: it identifies the route, finds the resource identifier in the request, and then checks whether the code verifies that the *authenticated user* actually owns or has permission to access that resource before returning it. Here's a realistic example from a course platform's enrollment API:

# api/enrollments.py
@app.route('/api/enrollments/<enrollment_id>/certificate', methods=['GET'])
@require_auth
def get_certificate(enrollment_id):
    enrollment = db.session.query(Enrollment).filter_by(
        id=enrollment_id
    ).first()

    if not enrollment:
        return jsonify({'error': 'Not found'}), 404

    return generate_certificate_pdf(enrollment)

The route requires authentication, which gives a false sense of safety - but it never checks that enrollment.user_id matches current_user.id. Any logged-in user can enumerate enrollment_id values and download certificates that were never issued to them. Claude Code flags this pattern specifically because it recognizes that @require_auth proves *who* is asking but says nothing about *what they're allowed to see* - authentication and authorization are different checks, and conflating them is one of the most common root causes of IDOR. The fix it typically proposes:

@app.route('/api/enrollments/<enrollment_id>/certificate', methods=['GET'])
@require_auth
def get_certificate(enrollment_id):
    enrollment = db.session.query(Enrollment).filter_by(
        id=enrollment_id,
        user_id=current_user.id
    ).first()

    if not enrollment:
        return jsonify({'error': 'Not found'}), 404

    return generate_certificate_pdf(enrollment)

The same class of reasoning catches horizontal privilege escalation in admin panels, mass-assignment bugs where a PATCH endpoint blindly applies req.body to a database record (letting a user set their own role field to admin), and missing tenant-isolation checks in multi-tenant SaaS applications - a bug category that is quietly devastating and almost never caught by automated scanners because "tenant isolation" is a business rule, not a syntax pattern.

Secrets, Sensitive Data Exposure, and Logging Mistakes

A surprising share of real-world security incidents trace back to something almost embarrassingly simple: a secret committed to source control, an API key hardcoded in a config file, or sensitive data written to application logs where it sits readable by anyone with log access. Claude Code is particularly effective here because grepping for api_key, secret, password, and token across a diff is cheap for it, but the real value is in judgment calls a regex can't make - like recognizing that a debug log statement is dumping an entire request object that happens to include an Authorization header.

// middleware/logger.js
app.use((req, res, next) => {
  console.log(`Incoming request: ${JSON.stringify(req.headers)}`);
  console.log(`Body: ${JSON.stringify(req.body)}`);
  next();
});

This looks like an innocuous debugging aid, but it logs the raw Authorization header (often a bearer token or session cookie) and the full request body on every single request - which, for a payment or auth endpoint, means credit card fields, passwords during login, or Stripe/Razorpay webhook secrets end up sitting in plaintext log files, often shipped to a third-party logging service with broad internal access. Claude Code flags this as sensitive data exposure and typically recommends redacting known-sensitive keys before logging:

const SENSITIVE_KEYS = ['authorization', 'cookie', 'password', 'card_number', 'cvv'];

function redact(obj) {
  const clone = { ...obj };
  for (const key of Object.keys(clone)) {
    if (SENSITIVE_KEYS.includes(key.toLowerCase())) {
      clone[key] = '[REDACTED]';
    }
  }
  return clone;
}

app.use((req, res, next) => {
  console.log(`Incoming request: ${JSON.stringify(redact(req.headers))}`);
  next();
});

It also catches related patterns that are easy to overlook: JWTs signed with a weak or hardcoded secret, encryption using ECB mode instead of GCM, passwords hashed with MD5 or unsalted SHA-256 instead of bcrypt/argon2, and .env files that get accidentally included in a Docker image because a .dockerignore entry is missing. None of these require exotic exploitation knowledge to explain - they require patient, systematic reading, which is exactly what an AI reviewer doesn't get tired of doing.

SSRF, Deserialization, and the Vulnerabilities Everyone Forgets

Server-side request forgery is the vulnerability class most likely to slip past a rushed manual review, because the vulnerable code often looks completely reasonable at first glance - a webhook handler that fetches a URL, an image proxy, a "test your webhook" feature. Consider:

@app.route('/api/webhooks/test', methods=['POST'])
def test_webhook():
    url = request.json.get('callback_url')
    response = requests.get(url, timeout=5)
    return jsonify({'status': response.status_code, 'body': response.text[:500]})

This feature lets a user paste a URL to verify their webhook endpoint is reachable - a common and legitimate feature on integration platforms. But there is no validation on url, which means an attacker can point it at http://169.254.169.254/latest/meta-data/iam/security-credentials/ on AWS and potentially exfiltrate cloud instance credentials, or target internal services on a private network that were never meant to be reachable from the internet. Claude Code catches this because it recognizes the "server makes an outbound request based on user-supplied input" pattern as inherently dangerous regardless of how benign the feature sounds, and it proposes validating against an allowlist of schemes and blocking private/link-local IP ranges before the request goes out - along with flagging that DNS rebinding can bypass a naive hostname check, so the validation needs to resolve the hostname first and check the resolved IP, not just the string.

Insecure deserialization gets similar treatment - Claude Code flags pickle.loads() on user-controlled data in Python, yaml.load() instead of yaml.safe_load(), and Java's ObjectInputStream.readObject() on untrusted input, all of which can lead to remote code execution and all of which are easy to write without realizing the danger, because the "unsafe" and "safe" versions of these functions often have nearly identical names and signatures.

Where Claude Code Falls Short (and Why That Matters)

None of this means a Claude Code review replaces a human security engineer, and treating it that way is itself a security risk. There are real limitations worth naming plainly:

  • No runtime context. Claude Code reads source code; it doesn't see your actual infrastructure, network topology, WAF rules, or runtime configuration. A vulnerability that looks exploitable in code might be mitigated by a firewall rule it can't see - and, more dangerously, a codebase that looks fine in isolation might be exploitable because of how it's deployed.
  • Business logic abuse requires business context you have to supply. It won't spontaneously know that your discount code system allows negative percentages, or that your referral program can be gamed by creating fake accounts, unless you tell it what "abuse" means for your product.
  • It can miss timing-based and race-condition vulnerabilities. Concurrency bugs, like a double-spend on a wallet balance from two simultaneous requests, are hard for any static reader (human or AI) to catch without explicitly reasoning about interleavings.
  • False negatives compound with codebase size. A 50-file diff gets a thorough read. A 500,000-line legacy monolith reviewed in one pass will get shallower coverage per file, even with a large context window - scope your reviews accordingly.
  • It won't stop you from asking the wrong question. If you never ask about SSRF, it may not volunteer it. Explicit checklists matter more than most people expect.

The right mental model is that Claude Code is an extremely diligent, extremely fast junior-to-mid-level security reviewer that never skips a file out of fatigue. It should feed into your existing process - PR review, SAST, dependency scanning, and periodic manual pentests - not replace the parts of that process that require human judgment about risk tolerance, business impact, and what happens after a vulnerability is found.

Building This Into Your Actual Development Process

The teams getting the most value out of AI-assisted security review aren't running one-off scans - they're wiring Claude Code into the places where vulnerabilities already tend to slip through:

  • Pre-merge, on every PR touching auth, payments, or data access. These are the files where an IDOR or injection bug does the most damage, so they deserve a dedicated review pass every time, not just when someone remembers to ask for one.
  • Before third-party integrations go live. New webhook handlers, OAuth flows, and payment provider callbacks are exactly the code shapes covered above - review them before the integration touches production traffic.
  • As part of onboarding new engineers to security expectations. Watching Claude Code explain *why* a pattern is dangerous, with a concrete exploit path, teaches junior developers security intuition faster than a static wiki page ever will.
  • Periodically against the whole codebase, scoped directory by directory. Rotate through the codebase over weeks rather than trying to review everything at once - this keeps each review focused enough to be thorough.

Security reviews fail most often not because nobody looked, but because nobody looked *carefully enough, consistently enough*. That is precisely the gap an AI reviewer is built to close - not by knowing something a good engineer doesn't, but by never getting tired, never skipping the boring file, and never assuming the previous reviewer already checked.

If you want to go deeper on wiring Claude Code into a real development workflow - from PR review automation to structured security prompts to building your own review checklists - our Claude Code Tutorial for Beginners course on TeachYou.ai walks through exactly this, starting from the fundamentals and building up to production-grade workflows you can drop into your own team's process.