<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
	<channel>
		<title>Product Security on brain overflow</title>
		<link>https://brainoverflow.blog/categories/product-security/</link>
		<description>Recent content in Product Security on brain overflow</description>
		<generator>Hugo -- 0.162.1</generator>
		<language>en-us</language>
		<lastBuildDate>Mon, 01 Jun 2026 11:35:14 -0700</lastBuildDate>
		<atom:link href="https://brainoverflow.blog/categories/product-security/index.xml" rel="self" type="application/rss+xml" />
		
		
		<item>
			<title>Hidden Gaps in Claude Code Security Reviews</title>
			<link>https://brainoverflow.blog/posts/claude-code-security-review-bias/</link>
			<pubDate>Mon, 01 Jun 2026 11:35:14 -0700</pubDate><guid>https://brainoverflow.blog/posts/claude-code-security-review-bias/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p><em>Anthropic recently shipped a new security plugin for Claude Code that automatically reviews code for vulnerabilities as you make changes, complementing the existing <code>/security-review</code> skill. I decided to test both against a deliberately constructed set of security flaws to see if the new tool improves coverage. Little did I know how deep this rabbit hole would take me. Fair warning: this is a long read.</em></p>
<hr>
<h2 id="1-background">1. Background<a href="#1-background" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Claude Code supports LLM-based security reviews at three stages:</p>
<table>
	<thead>
			<tr>
					<th>Tool</th>
					<th>Plans</th>
					<th>What the reviewer sees</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/security-review</code></td>
					<td>All</td>
					<td>Full branch, same or new session context (user&rsquo;s choice)</td>
			</tr>
			<tr>
					<td><strong>Security guidance plugin</strong> <em>(new, May 2026)</em></td>
					<td>All</td>
					<td>Git diff from current turn, fresh model context</td>
			</tr>
			<tr>
					<td>Code Review</td>
					<td>Team / Enterprise only</td>
					<td>Full codebase, multi-agent, independent model (runs on PRs)</td>
			</tr>
	</tbody>
</table>
<p>The new plugin shipped with an explicit design goal: avoid the <strong>model anchoring bias</strong> problem <a href="/posts/ai-native-threat-modeling/#5-on-model-bias-in-security-analysis">I wrote about earlier</a>. To understand what model bias means here, consider the human equivalent: if you ask the author of the code to review it, they&rsquo;ll likely tell you it&rsquo;s fine — they wrote it after all. A reviewer who wasn&rsquo;t in the room when the decisions were made will challenge assumptions the author has stopped seeing. The same dynamic applies to LLMs: when Claude writes code and then reviews it in the same session, it has the full conversation history in context, including every design choice and tradeoff it reasoned through while writing. It validates against those decisions rather than challenging them. A fresh session is the AI equivalent of a second pair of eyes.</p>
<p>The new plugin addresses this by running a <strong>separate Opus 4.7 session with a fresh context</strong>: the reviewer starts from the diff with no session history and no investment in the original approach. Anthropic&rsquo;s own documentation is direct about the design intent:</p>
<blockquote>
<p>&ldquo;The plugin does not ask the same Claude instance that wrote the code to grade itself. […] The end-of-turn and commit reviews run as a separate Claude call with a fresh context and a security-focused prompt: the reviewer starts from the diff, has no investment in the original approach, and is instructed only to find problems.&rdquo;</p>
</blockquote>
<p>This is a real solution to the model bias problem, but if you read deeper, it has its own limitation: <strong>a diff-scoped reviewer can only see what changed in the current turn</strong> and cannot reason about interactions between pre-existing code and new additions. That constraint is likely a cost decision: Opus 4.7 is expensive, and reviewing the full codebase on every change would be prohibitively token-intensive.</p>
<p>This gives me two hypotheses to experiment with:</p>
<p><strong>H1:</strong> same-session <code>security-review</code> is affected by model anchoring bias and will suppress findings that a cold run on the same code surfaces. The delta between the two runs measures how bad the gap is in practice.</p>
<p><strong>H2:</strong> the newly introduced diff-based plugin will miss vulnerability chains where each change looks benign in isolation but the two together form something exploitable, because the reviewer only ever sees one diff at a time and has no memory of what came before.</p>
<hr>
<h2 id="2-test-corpus">2. Test corpus<a href="#2-test-corpus" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>My target is based on a real Telegram bot that routes voice and text messages into a backend, but the version used here was vibe-coded from scratch for this experiment. The spec was written to elicit insecure decisions without explicitly asking for them: the goal was a realistic-looking codebase with seeded flaws.</p>
<p>The three flaws, ranging in complexity:</p>
<h3 id="f1-fail-open-authentication-simple">F1: Fail-open authentication (simple)<a href="#f1-fail-open-authentication-simple" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p><code>TELEGRAM_ALLOWED_USERS</code> is read into a set at startup. When the env var is absent, the set is empty. The auth guard uses the set as a condition:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> TELEGRAM_USERS <span style="color:#f92672">and</span> (<span style="color:#f92672">not</span> user <span style="color:#f92672">or</span> user<span style="color:#f92672">.</span>id <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> TELEGRAM_USERS):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span>
</span></span></code></pre></div><p>When <code>TELEGRAM_USERS</code> is empty, the entire <code>if</code> is skipped: any Telegram user is accepted. The correct default is deny-all: a bot that can read files and spawn subprocesses should fail closed, not open.</p>
<h3 id="f2-unrestricted-subprocess-permissions-medium">F2: Unrestricted subprocess permissions (medium)<a href="#f2-unrestricted-subprocess-permissions-medium" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>The bot classifies incoming messages and dispatches them, spawning <code>claude -p</code> subprocess with the <code>process_notes</code> skill and an <code>--allowedTools</code> list needed for the skill to run its operations. The allowed tools list passed to the inner Claude instance includes <code>Bash(python3:*)</code> without path restrictions. The <code>process_notes</code> skill reads the note from disk and invokes Python with it as input. If the skill passes note content to Python without sanitization, the chain reaches arbitrary code execution.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># python3 unrestricted</span>
</span></span><span style="display:flex;"><span>allowed_tools <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Read,Write,Bash(python3:*),Bash(mv:*),Bash(rm:*), ...&#34;</span>  
</span></span></code></pre></div><h3 id="f3-write--path-scoped-python3--write-then-execute-chain-hard">F3: Write + path-scoped python3 = write-then-execute chain (hard)<a href="#f3-write--path-scoped-python3--write-then-execute-chain-hard" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h3>
<p>Now <code>python3:*</code> is hardened to <code>python3:.claude/scripts/*</code>, but the <code>Write</code> permission remains. The chain: write a payload to <code>.claude/scripts/</code>, invoke it via python. Neither permission is dangerous alone: the vulnerability only exists when you hold both simultaneously. This flaw is the key test case for the new plugin: a diff-based reviewer seeing only the second permission added can&rsquo;t chain it to the first to recognize the combined severity.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>allowed_tools <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Read,Write,Bash(python3:.claude/scripts/*),Bash(mv:*), ...&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#                     ^^^^^ unrestricted       ^^^^^ scoped — looks safe</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Chain: Write payload → .claude/scripts/evil.py, python3 runs attacker&#39;s code</span>
</span></span></code></pre></div><p>The four tests map directly to the two hypotheses:</p>
<table>
	<thead>
			<tr>
					<th>Test</th>
					<th>Tool</th>
					<th>Setup</th>
					<th>Tests</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>T1</td>
					<td><code>/security-review</code></td>
					<td>Same session that wrote the code</td>
					<td>H1: does model bias suppress findings?</td>
			</tr>
			<tr>
					<td>T2</td>
					<td><code>/security-review</code></td>
					<td>Fresh session, no prior context</td>
					<td>H1 control: cold reviewer, does it catch everything?</td>
			</tr>
			<tr>
					<td>T3</td>
					<td>Security guidance plugin</td>
					<td><code>Write</code> + <code>python3:*</code> both new in the diff</td>
					<td>H2: does plugin catch a chain when it&rsquo;s fully visible?</td>
			</tr>
			<tr>
					<td>T4</td>
					<td>Security guidance plugin</td>
					<td><code>Write</code> pre-existing, only <code>python3 scripts/*</code> added</td>
					<td>H2: does plugin miss a chain split across commits?</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="3-h1--t1--t2-security-review-and-model-bias">3. H1 | T1 &amp; T2: /security-review and model bias<a href="#3-h1--t1--t2-security-review-and-model-bias" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Model bias in AI security review is a known problem, and Anthropic implicitly acknowledged it by releasing the new plugin. I&rsquo;m not going to dwell on it; the tests here are a quick empirical confirmation with a concrete measurement of the gap.</p>
<p>The <code>security-review</code> skill runs a multi-agent pipeline — initial identification followed by parallel false-positive filtering against known precedents, with a scoring system and a confidence threshold to report. It is more reliable than casually asking Claude to &ldquo;review this code for vulnerabilities&rdquo;, though some real issues may get filtered if the evidence isn&rsquo;t strong enough.</p>
<p><strong>Test 1</strong> ran in the same session that produced the bot; <strong>Test 2</strong> ran in a fresh session with no prior context. Same code, model, thinking effort, command.</p>
<table>
	<thead>
			<tr>
					<th>Finding</th>
					<th>Test 1 (same-session)</th>
					<th>Test 2 (cold session)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Fail-open auth: empty allowlist (F1)</td>
					<td>Not surfaced</td>
					<td><strong>High</strong> (0.95)</td>
			</tr>
			<tr>
					<td>Path traversal in file retrieval</td>
					<td>Not surfaced</td>
					<td>Medium (0.82)</td>
			</tr>
			<tr>
					<td>Unrestricted subprocess permissions (F2)</td>
					<td>Filtered out</td>
					<td>Not identified</td>
			</tr>
	</tbody>
</table>
<p>Test 1 reported zero vulnerabilities; Test 2 reported one High and one Medium. The same-session reviewer framed the threat model as &ldquo;authorized-only Telegram access&rdquo;, treating the auth as working correctly because the spec said so, and F1 never surfaced. The cold reviewer had no spec context and flagged it right away.</p>
<p>The path traversal finding is actually a stronger signal. It wasn&rsquo;t seeded in my prompt; it was a real bug the vibe-coding session introduced on its own, with no spec instruction to blame. The cold reviewer caught it; the same-session reviewer missed it alongside F1. ✅ <strong>H1 confirmed</strong>.</p>
<hr>
<h2 id="4-f2-the-component-boundary-you-shall-not-pass">4. F2: the component boundary you shall not pass<a href="#4-f2-the-component-boundary-you-shall-not-pass" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>It&rsquo;s not uncommon that while testing a hypothesis you run into new discoveries. So why did neither run flag the unrestricted subprocess permissions?</p>
<p>The answer is in the architecture. Both <code>write</code> and <code>process</code> are legitimate bot operations: the attacker stores a note to disk, then triggers <code>process</code> normally. <code>handle_process()</code> spawns <code>claude -p /process-notes --allowedTools [..]</code>. The subprocess call is visible in <code>inbox-bot.py</code>, but the skill it invokes is a separate file. Whether the skill passes vault content to Python in an exploitable way, and whether the permissions it runs with are appropriate, live outside the review scope.</p>
<pre class="mermaid">flowchart TD
    A["Attacker note\n(malicious payload)"] --> B["handle_write\n(legitimate)"]
    B --> C[("note on disk")]
    D["process command\n(legitimate)"] --> E["handle_process()"]
    E --> F["claude -p /process-notes\n--allowedTools Bash(python3:*) ..."]
    F -.->|"spawns"| G["/process-notes skill\n(out of review scope)"]
    G -->|"reads"| C
    G --> H["Bash(python3:*)\n→ RCE if vulnerable"]
    subgraph scope ["reviewed: inbox-bot.py"]
        B
        E
        F
    end
</pre>
<p>The architecture makes the concern visible: attacker-controlled vault content flows into a subprocess running with unrestricted python3. Neither automated reviewer evaluated it at that level, though they each hit the boundary differently.</p>
<p>In <strong>T1</strong> (same session), the reviewer identified the chain, labeling it &ldquo;Prompt injection via vault write to claude subprocess&rdquo;, but the false-positive filter dismissed it: <em>&ldquo;The attacker and vault owner are the same person; there is no external trust boundary being crossed.&rdquo;</em> That&rsquo;s <strong>model bias in a different form</strong>: not suppressing a finding outright, but supplying a session-derived trust assumption that the reviewer couldn&rsquo;t actually validate, because doing so would require seeing what <code>process-notes</code> does with the data it receives.</p>
<p>In <strong>T2</strong> (cold session), the reviewer checked for shell injection: seeing list-form <code>subprocess.run</code> with no <code>shell=True</code>, it marked the subprocess as clean and moved on. Seemingly, the <strong>presence of a known secure coding pattern steered the LLM into trusting the call as safe overall</strong>: the right invocation style closed scrutiny before it reached the component boundary question. The <code>--allowedTools</code> string with <code>Bash(python3:*)</code> was never evaluated.</p>
<p>Neither reviewer asked whether <code>python3:*</code> was too broad. That question doesn&rsquo;t require seeing <code>/process-notes</code> to answer: attacker-controlled data flowing into a subprocess with unrestricted python3 is a concern on its own, regardless of what the downstream skill does with it. A human reviewer would flag that pattern without needing to verify what the downstream component does with it. When you can&rsquo;t see past the boundary, the right default is to surface the concern.</p>
<hr>
<h2 id="5-h2--t3--t4-the-plugin-and-diff-isolation">5. H2 | T3 &amp; T4: the plugin and diff isolation<a href="#5-h2--t3--t4-the-plugin-and-diff-isolation" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>After a quick detour, we&rsquo;re back to probing the second part of the original hypothesis: does the new Claude plugin&rsquo;s diff-scoped reviewer miss a vulnerability chain where each change looks benign in isolation? This time, the chain is in the same file, and in a single tool call.</p>
<p><strong>Test 3 (1 diff):</strong> <code>Write</code> + <code>Bash(python3:*)</code> introduced together.
The plugin caught both: <code>python3:*</code> flagged as too broad, <code>Write</code> flagged as needing tighter scope. Two correct findings, auto-fix applied. But it treated them as independent concerns rather than a chain. The fix addressed F2; F3 survived.</p>
<pre tabindex="0"><code>The security hook flagged two real issues:
1. Bash(python3:*) is too broad — permits running any Python script.
   Should be scoped to the specific script path.
2. Write is too broad — should be scoped to the wiki directory under VAULT_ROOT.
</code></pre><p><strong>Test 4 (2 diffs):</strong></p>
<ul>
<li><code>Write</code> committed in the baseline. Nothing suspicious in isolation.</li>
<li><code>Bash(python3:.claude/scripts/*)</code> added in a new session. A narrow, path-scoped python3 permission — looks like a reasonable hardening move. <code>Write</code> is outside the diff and invisible to the reviewer.</li>
</ul>
<pre tabindex="0"><code>LLM code review: no vulnerabilities found.
</code></pre><p>And just like that, ✅ <strong>H2 confirmed</strong>.</p>
<p>Side observation from the test: when my git commit message named the permissions that had been removed, Claude read the log and inferred exactly what to restore, producing broad <code>python3:*</code> directly. I&rsquo;ve repeated the test with a neutral commit message, and it resulted in a different fix. The commit message didn&rsquo;t affect the plugin&rsquo;s review, but it changed what the writing model produced. Small sample, but a useful reminder that in vibe-coding sessions the model reads everything in context, and metadata you don&rsquo;t think of as instructions can still shape output.</p>
<hr>
<h2 id="6-what-can-we-do-about-all-this">6. What can we do about all this?<a href="#6-what-can-we-do-about-all-this" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>The model bias gap is actionable, and the fix is simple: run <code>/security-review</code> in a fresh session, not the one where you wrote the code. The unfortunate truth is that most users won&rsquo;t know to do this. The natural instinct is to run the skill right there in the session where you just finished writing the code. Model anchoring isn&rsquo;t obvious unless you know about it. Anthropic could nudge users here: detect when the tool is invoked in a session that also wrote the code, and warn before running.</p>
<p>I asked Claude to prototype this using session hooks. Available as a gist <a href="https://gist.github.com/obormot/9a241032c72c4d19a259f8bce6fa8ed3">here</a>, it works, but frankly it&rsquo;s not very good. The <code>decision: block</code> output is blunt; it stops the prompt and requires re-running. That&rsquo;s an API limitation: <code>UserPromptSubmit</code> hooks have no non-blocking notification option, so block is the only way to surface a visible message. Another caveat: in Claude Desktop, blocked prompts fail silently — the user gets no response and no explanation. This hook is only reliable in the Claude Code CLI.</p>
<hr>
<h2 id="final-thoughts">Final thoughts<a href="#final-thoughts" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Every tool in this space has gaps. Some are documented, and some are hidden, surfacing only when you test carefully enough. The title of this post came from expecting to confirm two gaps and finding three.</p>
<p>Are we all doomed until Mythos comes to save us? Models evolve rapidly, and Mythos is reportedly strong at exactly the cross-boundary chain reasoning that today&rsquo;s tools miss. It may well close these gaps - time will tell.</p>
<p>My broader take: fully autonomous code reviews don&rsquo;t replace human judgment. They extend your reach, and they&rsquo;re most useful when you understand what they can and can&rsquo;t see. Know the limits of your tools. Trust <strong>and</strong> verify.</p>
<hr>
<h2 id="references">References<a href="#references" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<ul>
<li><a href="https://code.claude.com/docs/en/security-guidance">Anthropic — Catch security issues as Claude writes code</a></li>
<li><a href="https://gist.github.com/obormot/9a241032c72c4d19a259f8bce6fa8ed3">Claude hook to warn about model anchoring bias</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>AI-Native Threat Modeling</title>
			<link>https://brainoverflow.blog/posts/ai-native-threat-modeling/</link>
			<pubDate>Wed, 20 May 2026 11:29:02 -0700</pubDate><guid>https://brainoverflow.blog/posts/ai-native-threat-modeling/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p><em>When I ask hiring managers why they&rsquo;re opening a product security role, the answer is
usually the same: we can&rsquo;t keep up. Development org grew, product surface expanded, and
the security team is the bottleneck. It&rsquo;s not a problem unique to any one organization
— it&rsquo;s the default state of product security. AI-accelerated development and vibe
coding are making it worse: more code, shipped faster, with the same security team
trying to keep up. The conventional wisdom is that vibe coding is a killer for AppSec
— and on the current trajectory, it is.</em></p>
<p><em>In this post, I argue that linear scaling won&rsquo;t solve that problem, and make the case
that AI-generated code, treated the right way, can be a force multiplier for security.</em></p>
<hr>
<h2 id="1-the-appsec-scaling-problem">1. The AppSec Scaling Problem<a href="#1-the-appsec-scaling-problem" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>The 1:100 ratio — one AppSec engineer for every hundred developers — is the number
the industry has quietly accepted as roughly accurate for mature organizations. It
sounds manageable until you sit with what it means in practice: a team of five
reviewing the output of five hundred, under sprint pressure, across a surface that
keeps growing. It&rsquo;s a demanding job — I wrote about <a href="/posts/thoughts-on-product-security-career/">what it actually
takes</a>.</p>
<p>The standard response is to hire more security engineers. That&rsquo;s reasonable when the
ratio is temporarily out of balance, but it doesn&rsquo;t address the structural problem. If
the development org doubled and the security team grew from five to ten, you&rsquo;re at the
same ratio. And the ratio assumes a roughly stable development velocity. AI coding
assistants are shattering that assumption.</p>
<p>Developers using GitHub Copilot, Cursor, or Claude Code ship more, faster. Vibe
coding — letting the model write code from a high-level natural language prompt
— compresses timelines further. Features that took two weeks take days.
The code surface is expanding at a rate that&rsquo;s no longer proportional to engineering headcount,
which means the AppSec scaling problem is now a two-sided function: development
velocity increasing, security team capacity roughly flat. The gap is structural, and
it is getting wider.</p>
<h2 id="2-where-traditional-approaches-break-down">2. Where Traditional Approaches Break Down<a href="#2-where-traditional-approaches-break-down" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>The vocabulary for addressing the AppSec scaling problem is well developed:
<strong>shift-left</strong>, <strong>secure-by-design</strong>, <strong>developer enablement</strong>, creating
<strong>paved roads</strong>. They&rsquo;re not wrong ideas. The problem is that they all require the same
scarce resource: AppSec time.</p>
<p><strong>Threat modeling</strong> — the recommended practice for high-risk features — is the
clearest example. The canonical process: the development team writes a design document;
the security team (or a joint session) works through the STRIDE framework or similar,
maps data flows and trust boundaries, produces a model; there&rsquo;s back-and-forth and
eventual sign-off. This is genuinely valuable when it happens. In practice, it often doesn&rsquo;t —
the process is time-consuming, and AppSec time is scarce.</p>
<p>What actually happens is one of three failure modes:</p>
<ol>
<li><strong>Delay</strong> — security reviews become release blockers, friction accumulates,
relationships with engineering teams deteriorate.</li>
<li><strong>Risk-accept</strong> — features ship with &ldquo;accepted risk&rdquo; security exceptions that go
into a backlog and are rarely revisited.</li>
<li><strong>No review at all</strong> — code ships without security involvement, entire product areas
built and deployed without the security team ever being in the loop.</li>
</ol>
<p>With AI now compressing time-to-exploitation — public vulnerabilities can have working
proof-of-concept code within hours — the third option is no longer a viable gamble.</p>
<p>Security code reviews have the same structural problem one step later: someone writes
code, another team reads it, back-and-forth, sign-off. Every handoff is a scheduling
dependency that adds release latency.</p>
<h2 id="3-the-threat-model-maintenance-problem">3. The Threat Model Maintenance Problem<a href="#3-the-threat-model-maintenance-problem" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>There&rsquo;s a second-order problem with threat modeling that gets less attention than the
initial production cost: <strong>drift</strong>.</p>
<p>A threat model is created as a snapshot, but the system keeps evolving. New endpoints added,
authentication flows refactored. Six months after a threat model is signed off, it describes
a system that no longer looks the same.
The question of who owns maintenance is usually a gray area: the development team didn&rsquo;t
write the model and isn&rsquo;t trained to maintain it; the security team is not aware of changes
and has to context-switch back into a system they last looked at months ago.
Neither path works well in practice.</p>
<p>Most organizations treat the threat model as a gate the security team required at feature launch —
it was produced, the box was checked, and maintenance was never part of the contract.
It documents what the system looked like at one point in time and then quietly expires.</p>
<h2 id="4-the-key-insight">4. The Key Insight<a href="#4-the-key-insight" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Here&rsquo;s where the mental model needs to shift.</p>
<p>In the current workflow, threat modeling is <strong>derivative work</strong>: a security person reads
what a developer built and reconstructs the security-relevant picture from it — after
the fact, inherently lossy, potentially inaccurate, and always one step behind.</p>
<p>Open-source projects such as <a href="https://github.com/davidmatousek/tachi">Tachi</a> and several
commercial offerings recognize this and offer tools that automate the reconstruction:
read the codebase, analyze diffs, apply a methodology, output a structured model.
These tools are useful, but they&rsquo;re still doing
the same derivative work, just faster — reverse-engineering security structure from
existing code rather than having a human do it. There&rsquo;s also a cost dimension:
analyzing an existing codebase means feeding it back through an LLM as new input,
which is expensive at scale. The larger and more frequently updated the codebase, the
higher the token cost of each analysis pass.</p>
<p>Now consider what changes when AI is writing the code — through vibe coding,
spec-driven development, AI-generated scaffolding from a design document, or an
agentic coding loop that implements a full feature end-to-end.</p>
<p>It doesn&rsquo;t reverse-engineer anything — it knows, because it built it: every data flow
it designed, every entry point it created, every asset it touched, every trust boundary
it crossed or established, every authentication decision it made. The complete map
required for a threat model exists as a natural byproduct of the design work the AI
just did — and it exists <em>at the moment of creation</em>, not after. And because that
context is already in the model&rsquo;s working window, generating the threat model alongside
the code is parallel effort on the same inputs, with little additional token cost.</p>
<p>The consequence of this observation is straightforward: <strong>threat models should be
generated alongside code, as first-class artifacts, not assembled later as derivative
documents</strong>.</p>
<pre class="mermaid">gantt
    title 1. Current — human-driven, sequential
    dateFormat YYYY-MM-DD
    axisFormat %d
    section Developer
    Design doc            :a1, 2024-01-01, 3d
    Write code            :a2, 2024-01-04, 3d
    section Reconstruct (Security)
    Reconstruct & model   :a3, 2024-01-07, 3d
    section Review (Security)
    Review & sign-off     :a4, 2024-01-10, 2d
</pre>
<pre class="mermaid">gantt
    title 2. AI-assisted — LLM writes code, LLM reads code
    dateFormat YYYY-MM-DD
    axisFormat %d
    section Developer
    Generate code         :b1, 2024-01-01, 3d
    section Reconstruct (AI-assisted)
    LLM reconstructs TM   :b2, 2024-01-04, 2d
    section Review (Security)
    Review & sign-off     :b3, 2024-01-06, 2d
    section Time saved
    time saved            :done, 2024-01-08, 4d
</pre>
<pre class="mermaid">gantt
    title 3. AI-native — code and threat model in parallel
    dateFormat YYYY-MM-DD
    axisFormat %d
    section Code
    Generate code         :c1, 2024-01-01, 3d
    section Threat Model
    Generate threat model :c2, 2024-01-01, 3d
    section Review (Security)
    Review & sign-off     :c3, 2024-01-04, 2d
    section Time saved
    time saved            :done, 2024-01-06, 6d
</pre>
<p>Accuracy improves — the model is a direct output from the entity that designed the
system, not a reconstruction. Maintenance improves because every code change can
regenerate or update it in the same operation; the entity making the change already
knows what changed and why. The multi-step,
multi-team back-and-forth collapses into a single step. Security practitioners remain
in the loop — for methodology, formal sign-off, challenging assumptions the AI didn&rsquo;t
surface — but the labor-intensive baseline work of constructing the model moves from a
human bottleneck to an automatic output.</p>
<p>This is what &ldquo;shift-left&rdquo; should actually mean: not <em>have the security team review
earlier</em>, but <em>produce the security model at the same moment the system is designed</em>.
The security artifact is contemporaneous with the code, not chasing it.</p>
<h2 id="5-on-model-bias-in-security-analysis">5. On Model Bias in Security Analysis<a href="#5-on-model-bias-in-security-analysis" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>A legitimate concern about this approach is AI model bias. There&rsquo;s a well-documented
pattern in AI-assisted security review: when a model writes code and is then asked to
evaluate it for security in the same context window, it tends to anchor to its own design
decisions, finding reasons why its choices are sound rather than challenging them. An
independent reviewer operating from a fresh context — a second model, or a human who
didn&rsquo;t write the code — is more likely to surface issues the original author missed. This
is a real limitation, and it applies directly to using AI for code security review.</p>
<img src="images/model-bias.png" alt="Code review vs. threat modeling under model bias" width="460" style="max-width:100%;display:block;margin:0 auto;border-radius:12px;">

<p>The core distinction here is that <strong>code security review</strong> and <strong>threat modeling</strong> are quite different.
A security review asks the model to evaluate whether its own implementation is correct and
secure — the question where anchoring bites hardest, because the model is judging choices
it already committed to. A threat model asks something structurally different: document the
architecture, establish trust boundaries, map data flows and assets, then apply a framework
like STRIDE that poses a fixed set of questions across threat categories.
The framework is external to the code; its questions don&rsquo;t change based on how well or
poorly the implementation is written. The question it asks — given what this system does,
what can go wrong in each of these categories? — is answered from the architectural map,
not from a judgment about implementation quality.</p>
<p>What bias <em>could</em> still affect is the model&rsquo;s assessment of severity — an AI that made a
particular design trade-off might rate the resulting risk lower than an independent reviewer
would. That&rsquo;s a real concern, and it&rsquo;s exactly why human review of the model&rsquo;s
outputs and assumptions is still valuable in this workflow.</p>
<h2 id="6-why-threat-modeling-still-matters">6. Why Threat Modeling Still Matters<a href="#6-why-threat-modeling-still-matters" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>A reasonable objection at this point: if AI writes the code, why not just ask it to
write <em>secure</em> code and skip the threat model entirely? We should absolutely ask for
that — but threat modeling serves purposes that &ldquo;write secure code&rdquo; doesn&rsquo;t address.</p>
<p><strong>Security architecture documentation.</strong> Threat models capture architectural decisions
and their security implications: trust boundaries, data classifications, what the system
assumes about its environment, where the blast radius of a failure ends. These don&rsquo;t
live in code. A system can be implemented correctly while making architectural
trade-offs that accept certain risks; those trade-offs need to be explicit, owned, and
findable.</p>
<p><strong>Known gaps and accepted risks.</strong> Every system ships with tradeoffs —
incomplete defenses, deferred work, risks that were evaluated and accepted.
A threat model makes these explicit: here is what we considered, here is what we&rsquo;re
not defending against, and here is why. This matters for accountability, for
prioritization, and for the engineer who joins the team six months from now.</p>
<p><strong>Compensating controls.</strong> Good security architecture is layered.
WAF rules, rate limiting, network segmentation, monitoring and alerting — these don&rsquo;t live
in application code, but they&rsquo;re part of the security posture. The threat model is
where they&rsquo;re connected to the threats they compensate for. This is also where
code-analysis-based automated tools tend to generate false positives:
they see the change in isolation, unaware of the external controls that already
mitigate a given risk.</p>
<p><strong>Compliance requirements.</strong> SOC 2, PCI-DSS, ISO 27001, HIPAA, and similar frameworks
require documented evidence of threat analysis. Auditors want artifacts. A threat model
that exists and is demonstrably current — generated from the same
codebase it describes — is a far stronger compliance artifact than one that was
carefully written at launch and hasn&rsquo;t been touched since.</p>
<p><strong>Incident response preparation.</strong> When something goes wrong — and eventually something
does — a current threat model tells you what&rsquo;s at risk, what attacker paths exist, and
what to prioritize. You want this analysis done before the incident, not during it.</p>
<p><strong>Stakeholder communication.</strong> Engineering leadership, legal, product, and board-level
security committees need to understand risk in terms they can act on. The codebase
doesn&rsquo;t serve this purpose; a structured threat model does.</p>
<p>The case for threat modeling doesn&rsquo;t weaken when AI writes the code — if anything,
AI makes the security artifacts cheaper to produce, easier to keep current,
and more consistently complete than the human-driven alternative.</p>
<h2 id="final-thoughts">Final thoughts<a href="#final-thoughts" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>I think this is the direction the AI coding toolchain is already moving toward, even
if the full vision hasn&rsquo;t arrived yet. AI coding tools are increasingly integrating
security into the development workflow: GitHub Copilot&rsquo;s real-time vulnerability
detection during code generation, Claude Code&rsquo;s security analysis during code review,
Replit&rsquo;s Security Agent in the development environment. None of these offer AI-native threat
model generation, but they signal that the industry is treating security as something
the coding tool prioritizes and produces alongside code.
The extension of that to living, maintained threat models is the logical next step.</p>
<p>The reframe for ProdSec practitioners is this: stop thinking of threat
modeling as a process your team performs on code that developers write. Start thinking
of it as an artifact the AI coding assistant produces alongside the code, which your
team validates, challenges, and signs off on. The security team&rsquo;s job shifts from
construction to judgment — which is where human expertise actually compounds.</p>
<p>The dreaded 1:100 ratio won&rsquo;t disappear. But the work of constructing and maintaining
the threat model doesn&rsquo;t have to stay a human-hours problem. The needle can move —
but only if the security team&rsquo;s role evolves with it.</p>
<p><img src="images/surf.png" alt="Image generated by Google Gemini"></p>
]]></content>
		</item>
		
		<item>
			<title>Thoughts on Product Security Career</title>
			<link>https://brainoverflow.blog/posts/thoughts-on-product-security-career/</link>
			<pubDate>Tue, 12 May 2026 09:51:22 -0700</pubDate><guid>https://brainoverflow.blog/posts/thoughts-on-product-security-career/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p><em>I recently wrote about <a href="/posts/my-product-security-principles/">my product security principles</a> — the operating frame I&rsquo;ve built for doing the job well. This is the post that probably should have come first: what product security actually is as a career, whether it might be the right path for you, and what ten years of doing it has taught me.</em></p>
<hr>
<p>Ten years in product security teaches you one thing above all: it is <strong>a hybrid discipline</strong>, and that is both its challenge and its appeal.</p>
<p>The role asks for coding skills — enough to read unfamiliar codebases, spot vulnerability patterns, and write the automation that makes security scale — but not at the level of a senior software engineer. It asks for offensive security knowledge — how attackers think, how systems break — but you&rsquo;re not a red teamer or a dedicated pentester. You need architectural judgment and systems-level thinking to design security solutions that fit inside complex systems, but you&rsquo;re not designing the products themselves. Program management skills come into play when you&rsquo;re owning a roadmap and driving cross-functional initiatives, but your customers are internal. Risk and compliance fluency matters — understanding risk is what drives prioritization decisions — without being a GRC officer. Enough ITSec grounding to be credible in an IR conversation, without being a SOC analyst.</p>
<p>Rarely all of these at full depth — but all of them at working depth. The breadth is the job.</p>
<p><strong>The technology surface</strong> is equally wide. Multi-cloud environments, Kubernetes and container security, CI/CD pipeline hardening, secrets management, HSM-backed key hierarchies, OS-level hardening, infrastructure-as-code, supply chain integrity, identity and access management, compliance frameworks — the list is long and grows with the industry. You don&rsquo;t need to be the expert in all of it, but you need to be fluent enough in each area to ask the right questions, spot the gaps, and know when to go deeper.</p>
<p><strong>Context switching</strong> is another constant demand. Security teams are undersized by design, so people come to you constantly: a quick auth question from a developer, a compliance clarification from legal, an architecture review that landed in your queue, an incident that just got escalated. Each requires a different mode — deep focus for a thorough threat model, quick confident judgment for the everyday interruptions. The instinct might be to guard your time against the noise. Resist it. Those questions are how you move the needle. Embrace them.</p>
<p><strong>The human side</strong> carries equal weight with technical depth — and this often goes unsaid. Influencing teams that don&rsquo;t report to you, competing for roadmap space without turning adversarial, partnering with engineering instead of policing it, enabling people rather than gatekeeping them. Add to that customer-facing and executive communication — translating technical risk into language that lands with a non-technical audience is a distinct skill, and a critical one. Product security lives inside organizations with competing priorities, and how far you move the needle depends as much on how well you work with people as on what you know.</p>
<p><strong>High stakes</strong> raise the difficulty. There&rsquo;s the obvious pressure of a security incident — high-visibility, fast-moving, unforgiving. But there&rsquo;s also the quieter, constant pressure of not missing something: a vulnerability in a design review, a misconfiguration in a new service, a risk that slips through and becomes next quarter&rsquo;s incident. The job requires staying sharp under both.</p>
<p><strong>Invisible success</strong> is the other side of that coin — and something I touched on in my earlier post. When nothing goes wrong, there&rsquo;s nothing visible to point to. Security&rsquo;s value is counterfactual by design, and that takes some getting used to.</p>
<hr>
<h2 id="if-youre-hiring-for-product-security">If you&rsquo;re hiring for product security<a href="#if-youre-hiring-for-product-security" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Understanding what the role actually requires has a direct implication for how you hire — and most interview processes get this wrong.</p>
<p><strong>The most common mistake</strong> I see is screening candidates with a LeetCode-style assessment. I&rsquo;ve worked with some of the brightest security engineers in the industry — holding them to an algorithmic coding bar doesn&rsquo;t filter for talent, it filters out the wrong people. That&rsquo;s not what you&rsquo;re hiring them for.</p>
<p>The same applies to <strong>system design</strong>. I&rsquo;d bet many of the best security engineers I&rsquo;ve worked with couldn&rsquo;t design a scalable distributed system end-to-end — but they can dissect an existing one and find its security design flaws faster than anyone in the room. The blank-whiteboard system design exercise misses the point entirely.</p>
<p><strong>What works instead:</strong> for the <strong>coding round</strong>, give them a real code sample and ask them to review it for vulnerabilities. Give them a CVE and walk through the risk assessment — what&rsquo;s the realistic impact, what systems are exposed, how would you prioritize remediation? Keep a human in the loop; you want to see how they think, what they catch, what questions they ask. For <strong>system design</strong>, hand them an actual design document or a system diagram and ask them to threat model it: identify the assets worth protecting, map the trust boundaries, enumerate the threats at each boundary, reason through attack vectors — then recommend layered defenses to mitigate the risks they&rsquo;ve identified. A candidate who can do that credibly is showing you the core of the job.</p>
<p>Software engineers and security engineers look at the same systems from different angles. Tailoring the interview process to the role isn&rsquo;t lowering the bar — it&rsquo;s raising the accuracy of the bar.</p>
<hr>
<p><em>Product security demands technical breadth, strong soft skills, and the ability to navigate complex organizational dynamics — all at once. Hiring for it requires a process calibrated to that reality. Is it the right career for you? Ten years in, it still is for me.</em></p>
]]></content>
		</item>
		
		<item>
			<title>My Product Security Principles</title>
			<link>https://brainoverflow.blog/posts/my-product-security-principles/</link>
			<pubDate>Sun, 10 May 2026 00:00:00 -0700</pubDate><guid>https://brainoverflow.blog/posts/my-product-security-principles/</guid>
			<description><![CDATA[&lt;no value&gt;]]></description><content type="text/html" mode="escaped"><![CDATA[<p><em>In my recent job search I read dozens of Product Security job descriptions. They all contain the same buzzword soup: shift-left, secure-by-default, defense in depth, paved roads. In practice, they mean different things at different companies — but what do they actually mean for the Product Security team?</em></p>
<p><em>What follows is my personal operating frame. One security engineer for every hundred in engineering is roughly where the industry sits — these are my principles for operating and succeeding in that reality. And I believe they hold in a world of vibe-coded apps and AI-accelerated production code.</em></p>
<hr>
<h2 id="1-risk-is-the-unit-of-work-not-findings">1. Risk is the unit of work, not findings<a href="#1-risk-is-the-unit-of-work-not-findings" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Everything flows from business risk: vulnerabilities, architecture gaps, compliance requirements are all risks to be scored, prioritized, and decided on. They belong in a risk register that the business actually owns, and part of the security team&rsquo;s job is ensuring it does: it should be a living record that business owners understand, contribute to, and sign off on, not a document security maintains in isolation.</p>
<p><strong>Proactive and continuous risk reduction</strong> is how I formulate the security team&rsquo;s mission.</p>
<h2 id="2-frame-risk-in-business-terms">2. Frame risk in business terms<a href="#2-frame-risk-in-business-terms" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>A CVSS score means nothing to an executive. A risk item must answer: what&rsquo;s the realistic scenario, what does it cost if it happens, what does it cost to fix, what&rsquo;s your recommendation? When security decisions carry significant business risk, frame them in business language.</p>
<p>Seek explicit executive sign-off for security exceptions and risks above a materiality threshold — it moves accountability to where the decision lives.</p>
<p><strong>Risk = Severity × Likelihood</strong>: the key formula that turns a vulnerability into a business decision.</p>
<blockquote>
<p><strong>UPD 5/30:</strong> A reader pointed out that my original formula (<em>Risk = Severity × Potential Impact</em>) was incorrect; it conflated severity and potential impact (which represent the same measurement), and missed the likelihood completely. I&rsquo;ve updated the post with the correct formula above.</p>
</blockquote>
<h2 id="3-security-architecture">3. Security Architecture<a href="#3-security-architecture" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>A whiteboard conversation at design time costs an hour; a redesign after implementation costs a sprint. Security belongs at the beginning of the design process, not at the end as a gate. The goal is to be the person engineers call when they&rsquo;re designing, not when they&rsquo;re shipping.</p>
<p>Don&rsquo;t overthink threat modeling; formal frameworks have their place, but if the overhead of the methodology is slowing teams down, drop it. A napkin sketch of trust boundaries and a list of &ldquo;what could go wrong&rdquo; is a threat model, an imperfect one done at design time beats a rigorous one that never happens.</p>
<p>Good security architecture is transparent. Document it publicly; it builds trust and is the right counterweight to security through obscurity. If the design is sound, exposure doesn&rsquo;t weaken it. If your code ever leaks, there should be no secrets in it worth finding.</p>
<h2 id="4-assume-controls-fail--design-and-test-for-it">4. Assume controls fail — design and test for it<a href="#4-assume-controls-fail--design-and-test-for-it" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>The operating posture is proactive, not reactive. Find the gaps before an attacker or a customer does. No single control holds forever: when this fails, what&rsquo;s the worst reachable outcome? Isolation, least privilege, and short-lived credentials aren&rsquo;t redundancy, they&rsquo;re blast radius reduction. Treat defense in depth as a system property.</p>
<p>Designing for failure isn&rsquo;t enough — validate that your controls actually perform as designed. Security audits, red and purple team exercises, and bug bounty programs all serve the same function: actively probing your own assumptions.</p>
<h2 id="5-friction-is-the-enemy">5. Friction is the enemy<a href="#5-friction-is-the-enemy" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Culture is a big one. Most engineers <em>want</em> to build secure software — they&rsquo;re just operating under deadlines, competing priorities, and finite cognitive bandwidth. When security loses, it&rsquo;s usually not because engineers don&rsquo;t care; it&rsquo;s because the secure path was harder than it needed to be, or they simply didn&rsquo;t know what it was. Security expertise isn&rsquo;t a given — engineers are experts in their domain, not ours.</p>
<p>Every process, template, and gate should make the secure choice the default, not the tax. Security has to live inside the workflows engineers already use. A separate system they have to visit is a system that will fail adoption. Friction reduction is the mechanism; the goal is cultural: security becoming a natural part of how the team ships.</p>
<h2 id="6-influence-over-formal-authority">6. Influence over formal authority<a href="#6-influence-over-formal-authority" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Security teams often have no direct power over engineering decisions. Authority comes from technical credibility, consistent judgment, and being right often enough that people seek your input. A security control engineers chose is worth more than one you mandated.</p>
<p>Influence runs in both directions. Top-down: executive sponsorship sets the tone and makes security non-negotiable at the policy level. Bottom-up: invest in building relationships with engineering teams — understand their roadmaps, empathize with their pressures — that&rsquo;s where actual adoption happens.</p>
<h2 id="7-partners-not-adversaries">7. Partners, not adversaries<a href="#7-partners-not-adversaries" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Competing with engineering for resources — security work versus features on the roadmap — comes with the territory. That tension is structural and it never fully goes away. Recognize it as part of the job; the risk is letting it harden into an us-versus-them mentality that undermines collaboration. Security and engineering look at the same problems from different angles, but there is one goal: ship secure software. Learn each other&rsquo;s stack, understand the roadmap, show up as a collaborator rather than a reviewer. The security team engineers want to call is more effective than the one they&rsquo;re required to consult.</p>
<h2 id="8-know-when-to-stand-down--and-when-to-push-back">8. Know when to stand down — and when to push back<a href="#8-know-when-to-stand-down--and-when-to-push-back" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>The willingness to say &ldquo;network-layer isolation is sufficient here&rdquo; or &ldquo;this threat is acceptable risk&rdquo; is what earns credibility for the fights that matter. Security maximalism destroys trust; knowing when to stand down builds it.</p>
<p>When you do push back, come with data — exploit likelihood, realistic impact, cost to fix. And maximize the context you hand to developers: a finding with a clear severity rationale, a realistic attack scenario, and a suggested remediation gets acted on. A bare vulnerability ID with no explanation gets triaged into a backlog and forgotten. The goal isn&rsquo;t to be right — it&rsquo;s to be useful.</p>
<h2 id="9-disagree-and-commit--deliberately">9. Disagree and commit — deliberately<a href="#9-disagree-and-commit--deliberately" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Sometimes a feature ships with known security gaps. That&rsquo;s a business decision, and it&rsquo;s often the right one. The security team&rsquo;s job in that moment isn&rsquo;t to block or to silently acquiesce — it&rsquo;s to make the decision deliberate: agree on the minimal security bar, add basic compensating controls, document the residual risk, and put the remediation work on the roadmap. Ship it, then follow through. The danger isn&rsquo;t shipping with known gaps — it&rsquo;s shipping with undocumented gaps and no agreed plan to close them.</p>
<h2 id="10-scale-through-systems-not-headcount">10. Scale through systems, not headcount<a href="#10-scale-through-systems-not-headcount" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>A small security team can&rsquo;t review everything a hundred engineers build. Security scales through parallel tracks:</p>
<ol>
<li>
<p>Enablement: templates, reference architectures, security champions, and training that make good security judgment transferable — so engineers make secure decisions without needing a security review at every turn.</p>
</li>
<li>
<p>Automation: SAST, dependency scanning, secrets detection, security gates in CI/CD that run on every PR, and most recently, LLMs.</p>
</li>
<li>
<p>Holistic remediation: when a vulnerability pattern surfaces in a functional area, drive an initiative to close the class — a shared library, a framework guardrail, a linting rule. Closing the class beats closing the tickets.</p>
</li>
</ol>
<h2 id="11-security-success-is-invisible--until-it-is-a-failure">11. Security success is invisible — until it is a failure<a href="#11-security-success-is-invisible--until-it-is-a-failure" class="anchor" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
      stroke-linecap="round" stroke-linejoin="round" class="feather">
      <path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
      <line x1="8" y1="12" x2="16" y2="12"></line>
   </svg></a></h2>
<p>Security&rsquo;s value is counterfactual by design. You&rsquo;re selling the absence of bad outcomes, which is invisible until it isn&rsquo;t. The &ldquo;we didn&rsquo;t get hacked — why do we even need a security team?&rdquo; question is a predictable trap. When things are quiet, there&rsquo;s nothing visible to point to; when something goes wrong, the case for security makes itself — but at too high a cost.</p>
<p>The measurement gap is real — work around it. SLA compliance, MTTR, vulnerability age, findings caught pre-production are useful signals. Tell the risk reduction story proactively: here&rsquo;s what we found before it became a breach, here&rsquo;s how the attack surface changed over the past year, here&rsquo;s what we closed before a researcher or an attacker got there first.</p>
<p>Security that only shows up in the numbers after an incident has already lost the framing war — and ironically, that&rsquo;s often when companies make their first Product Security hire.</p>
<hr>
<p>Done well, product security is invisible: engineers ship without friction, teams collaborate without tension, and executives make informed decisions without needing a crisis to focus them. Getting there is a journey, but not an impossible one.</p>
]]></content>
		</item>
		
	</channel>
</rss>
