Skip to main content

Command Palette

Search for a command to run...

How I Use Claude Code to Fix Security Vulnerabilities

Updated
11 min read
How I Use Claude Code to Fix Security Vulnerabilities

About this article

This article documents how I use Claude Code in my CVE research workflow — specifically, how I handle the moment Claude gives me a fix recommendation for a vulnerability I've found through static analysis.

Over many sessions I noticed a recurring pattern: when I ask Claude to patch a vuln, it often returns 2–3 options instead of one answer. At first this looked like a weakness — I just wanted "the fix." After enough real cases, I realized it's actually a useful behavior most of the time (real trade-offs exist in security work), and a footgun the rest of the time (Claude hedging because my prompt was vague).

What follows is the framework I now use to work with Claude on security fixes:

  • §1–§2 — Why multiple-option answers happen and the 5 trade-off shapes I see most often

  • §3 — A 5-axis evaluation framework for picking the right option

  • §4–§5 — How to tell genuine trade-offs from Claude hedging

  • §6 — The step-by-step workflow I run when Claude gives me options

  • §7 — How I write prompts that prevent hedging in the first place

If you're using AI to help with security fixes — whether for CVE research, internal pentest reports, or just routine vulnerability triage — this should save you some of the trial and error it took me to figure out.


1. The reality: most security fixes don't have "one right answer"

Unlike ordinary bug fixes (broken logic → patch it), security fixes usually have multiple valid approaches because:

  • Each fix lives at a different layer of the pipeline

  • Each fix has different trade-offs (security strength vs UX vs compatibility vs performance)

  • Claude doesn't know what the project prioritizes → it offers options so you can decide

This isn't Claude being lazy or unsure — it's Claude respecting your domain knowledge about the project's context.


2. Common trade-offs when Claude offers 2 options

A. Minimal fix vs Defense in depth

Example — Stored XSS in a Twig template:

// Vulnerable
{{ user.bio | raw }}

Option A — Minimal (fix at the sink):

{{ user.bio }}  // remove |raw, let Twig auto-escape
  • Smallest diff, easiest to review

  • No change to data structure

  • If another template also renders bio → still vulnerable

Option B — Defense in depth (sanitize at input + escape at output):

// In the controller (input)
\(user->bio = strip_tags(\)request->bio, '<b><i><a>');

// In the template (output)
{{ user.bio | raw }}  // data is now clean
  • Protection at 2 layers — safe even if |raw is accidentally added elsewhere

  • Larger diff, touches input validation

  • If strip_tags strips too aggressively → user loses data

Claude offers 2 options because:

  • Security-conscious maintainers usually prefer B (defense in depth)

  • Maintainers who prioritize a small, easy-to-review PR prefer A

  • Claude doesn't know the project's culture


B. Library/framework approach vs Manual approach

Example — SQL Injection in a raw PHP query:

// Vulnerable
\(db->query("SELECT * FROM users WHERE email = '\)email'");

Option A — Use the framework's ORM/Query Builder:

// Laravel Eloquent
User::where('email', $email)->first();
  • Idiomatic for a Laravel codebase

  • Auto-parameterized

  • If raw queries are used elsewhere → inconsistent

Option B — Plain PDO prepared statement:

\(stmt = \)db->prepare("SELECT * FROM users WHERE email = ?");
\(stmt->execute([\)email]);
  • Minimal change — still raw SQL, just adds binding

  • No ORM knowledge required

  • If the project already uses ORM → goes against the established pattern

Claude offers 2 options because it can't tell the project's idiomatic style — reading 1-2 files isn't enough to be sure.


C. Strict vs Permissive validation

Example — SSRF in a webhook URL:

// Vulnerable
\(response = file_get_contents(\)request->webhook_url);

Option A — Allowlist (strict):

$allowed_hosts = ['api.partner-a.com', 'webhook.partner-b.com'];
\(host = parse_url(\)url, PHP_URL_HOST);
if (!in_array(\(host, \)allowed_hosts)) {
    throw new Exception('Host not allowed');
}
  • Most secure — only pre-approved hosts allowed

  • Kills the "arbitrary webhook" feature — poor UX

Option B — Blocklist (block private IP ranges):

\(ip = gethostbyname(parse_url(\)url, PHP_URL_HOST));
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
    throw new Exception('Private IP not allowed');
}
  • Keeps arbitrary-webhook functionality

  • Blocks 99% of common SSRF attacks

  • DNS rebinding can still bypass if you don't re-resolve

Claude offers 2 options because this is a product decision, not a pure technical decision. "Webhook" means different things for different products.


D. Reject vs Sanitize vs Encode

Example — XSS in a comment system:

Option A — Reject (block input):

if (preg_match('/<script|on\w+=/i', $comment)) {
    throw new Exception('Invalid input');
}

Option B — Sanitize (clean input):

\(comment = HTMLPurifier::clean(\)comment);  // strip dangerous tags, keep safe HTML

Option C — Encode (output safely):

echo htmlspecialchars($comment);  // keep raw input, escape when rendering

Trade-off:

Option A Option B Option C
UX Poor (false rejects) Medium Best
Security Weak (regex bypass) Strong Strongest
Performance Fast Slow (HTMLPurifier is heavy) Fast
Implementation cost Low High (new dependency) Low

This is a classic security architecture decision — there's no right or wrong, only what fits the context.


E. Breaking change vs Backward compatible

Example — JWT auth bypass via alg: none:

# Vulnerable
jwt.decode(token, key, algorithms=['HS256', 'RS256', 'none'])

Option A — Breaking (accept only one algorithm):

jwt.decode(token, key, algorithms=['RS256'])  # drops 'none' and HS256
  • Most secure

  • Existing HS256 tokens stored in clients will fail → mass re-login required

Option B — Non-breaking (keep HS256, drop 'none'):

jwt.decode(token, key, algorithms=['HS256', 'RS256'])
  • Fixes the vuln, breaks nothing

  • HS256 still carries risk if the secret is leaked

Claude offers 2 options because it doesn't know:

  • How many users hold HS256 tokens

  • Whether mass re-login is acceptable

  • Whether a key rotation mechanism exists


3. How to read Claude's options — an evaluation framework

When Claude offers 2-3 options, evaluate along 5 axes:

  1. Security strength — Which option closes the hole more tightly?

  2. Diff size — Which is easier to review?

  3. Compatibility risk — Which might break users/clients?

  4. Idiomatic with the project — Which matches the codebase's style?

  5. Maintainability — Will a new developer understand this 6 months from now?

Rules of thumb for responsible disclosure PRs:

Situation Pick
First PR to a large project Minimal fix — easier to accept; you can follow up with a broader PR
Critical vuln, exploit already in the wild Defense in depth — close as many layers as possible
Project with a clear style guide Idiomatic option — even if the diff is larger
Library/SDK with many downstream consumers Non-breaking option — avoid forcing mass migration

4. When Claude SHOULDN'T offer 2 options (an anti-pattern)

Sometimes Claude offers 2 options but only one is actually correct — this is a sign Claude is hedging due to lack of confidence:

Option A: use mysqli_real_escape_string

Option B: use prepared statements

This isn't a real trade-off — prepared statements are always better. Claude offered A out of caution. You should push Claude to pick one and explain.

Option A: hash passwords with MD5

Option B: hash passwords with bcrypt

MD5 for passwords is never acceptable. This isn't a choice.

When you suspect Claude is hedging:

  • Ask: "If you had to pick one, which would you choose and why?"

  • Ask: "Is there industry consensus on this?"

  • Check the OWASP cheat sheet — if there's a clear recommendation → follow it


5. When offering 2 options actually has value

It has value when:

  • The 2 options sit at different layers (input vs output, framework vs manual)

  • The 2 options carry obvious trade-offs that anyone can see

  • The 2 options require domain knowledge you have but Claude doesn't (business logic, user base, deployment context)

  • The project maintainer has a strong opinion that Claude isn't aware of

It has no value when:

  • The 2 options differ only in syntax

  • One of the 2 options is clearly less secure

  • Claude is offering them just to hedge against uncertainty


6. A workflow for handling 2-option recommendations

Step 1: Read the trade-off Claude presents
"Option A: minimal, Option B: defense in depth"
→ Is this trade-off real?

Step 2: Map it onto the project's context
- Does the project have a strong security culture? (read SECURITY.md, CHANGELOG)
- Does it have a style guide? (read CONTRIBUTING.md)
- What kind of PRs does the maintainer typically accept? (read recent merged PRs)

Step 3: Ask Claude again, this time with context
"This project uses idiomatic Laravel with Sanctum for auth. Given that, which option do you pick?"

Step 4: Decide
- First PR: go minimal
- Already discussed with the maintainer: go with what they want

Step 5: Document the choice in the PR description
"Considered Option B (defense in depth) but chose A for minimal diff. Happy to extend if maintainer prefers."
→ Shows you thought about it — not a copy-paste

7. Writing better prompts to reduce hedging in the first place

Section 4 covered how to spot hedging after it appears. Better: prevent it. Most hedging traces to three gaps — no constraints, no project context, or you explicitly asked for options.

7.1 Prompts that work

Force a decision with constraints:

❌ "How should I fix this XSS?"

✅ "Fix this XSS with the smallest diff. First-time PR to a project I don't maintain. Pick one and apply it."

Bake context into the prompt:

❌ "Fix the SSRF in webhook.php."

✅ "Fix the SSRF in webhook.php. B2B integration platform — allowlist won't work. Block private IPs with the right PHP API. Apply the fix."

Ask for a recommendation, not options:

❌ "What are my options for fixing this JWT bypass?"

✅ "Recommend the single best fix. Mention the trade-off you rejected in one sentence."

Set a confidence threshold:

✅ "Pick one fix and apply it. Mention an alternative only if it's 30% better on security, maintainability, or performance."

7.2 A reusable template

Fix [vulnerability class] in [file].
Context: [framework, scale, audience — 1 sentence].
Constraints: [smallest diff / defense in depth / breaking-change OK?].
Decision: pick one fix and apply it. Alternatives only if substantially better.

7.3 Stop phrases that invite hedging

Stop phrase Use instead
"What are my options" "Recommend one"
"How could I fix this" "Apply the best fix"
"What do you think?" "Pick one and explain"
"Compare X and Y" "Which is right here, and why?"

7.4 When you DO want options

Sometimes the trade-off is a real product decision (e.g., Section 2.C). Ask explicitly and bound the answer:

✅ "Give me exactly two fixes: one allowlist, one blocklist. For each: security strength, UX impact, one failure mode. No third option."


8. What this workflow has produced

Published advisories

Pull Request


TL;DR

  • Claude offers more than one options because security fixes have real trade-offs

  • 5 common trade-off categories: minimal vs defense-in-depth, library vs manual, strict vs permissive, reject vs sanitize vs encode, breaking vs backward-compat

  • Evaluate options along 5 axes: security, diff size, compatibility, idiomatic fit, maintainability

  • Watch for hedging — when Claude offers 2 options but one is clearly wrong, push it to pick one

  • The best prevention is upstream — write prompts that constrain and decide (see §7)

  • For a first disclosure PR: default to the minimal fix — easier to accept, less likely to spark debate

  • Always document in the PR why you picked that option — maintainers respect a reasoned choice

How I Use Claude Code to Fix Security Vulnerabilities