<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mihirajmera.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mihirajmera.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-11-03T10:38:28-05:00</updated><id>https://mihirajmera.github.io/feed.xml</id><title type="html">Mihir Ajmera</title><subtitle>GRC engineer proving that AI systems can be governed with the same rigor as financial controls. Open-source tools and writing on AI agent access, LLM red-team evidence and compliance-as-code.</subtitle><author><name>Mihir Ajmera</name></author><entry><title type="html">A regulator just wrote the audit program for AI agents</title><link href="https://mihirajmera.github.io/blog/a-regulator-just-wrote-the-audit-program-for-ai-agents/" rel="alternate" type="text/html" title="A regulator just wrote the audit program for AI agents" /><published>2026-11-03T00:00:00-05:00</published><updated>2026-11-03T00:00:00-05:00</updated><id>https://mihirajmera.github.io/blog/a-regulator-just-wrote-the-audit-program-for-ai-agents</id><content type="html" xml:base="https://mihirajmera.github.io/blog/a-regulator-just-wrote-the-audit-program-for-ai-agents/"><![CDATA[<p>Yesterday the Hong Kong Monetary Authority launched Project SAGE, a program for banks to trial AI agents in payments and treasury, and alongside it published a <a href="https://www.hkma.gov.hk/media/eng/doc/key-information/press-release/2026/20261102e3a1.pdf">discussion paper on agentic AI governance in banking</a>.</p>

<p>The paper is careful about its status. It “creates no new obligations” and asks for comments by February 1, 2027. But it’s the most concrete regulatory document on AI agents I’ve read, for one reason: almost every section ends with a sentence starting “the practical test is whether.” Those sentences are audit tests. Read together, they’re an audit program for AI agents, and almost none of it is specific to banks.</p>

<p>One note on reading it: the paper uses “AI” to mean <em>authorized institution</em>, the HKMA’s term for a bank, and writes artificial intelligence as “A.I.”</p>

<div class="figure-row"><div class="amber"><b>1</b><span>named accountable owner per agent deployment, from the business, not IT</span></div><div class="red"><b>5</b><span>agent failures the HKMA asks whether to treat as reportable incidents</span></div><div class=""><b>Feb 1</b><span>2027 deadline for comments on the paper</span></div></div>

<h2 id="five-practical-tests">Five practical tests</h2>

<h3 id="1-can-you-name-who-answers-for-each-agent">1. Can you name who answers for each agent?</h3>

<p>Before an agent goes into production, the paper wants the bank to write down its “business intent”: what it’s for, what it can do, which data and tools it can touch, what data can leave the firm, its limits, and when its authority should be withdrawn.</p>

<p>Then one person owns it, and that person should normally come from the business, not technology. The paper is blunt that accountability “should not default to technology functions solely because agentic A.I. is involved.” Technology owns the build, the logs and the stop mechanism. A separate control owner, independent of the build, owns testing and challenge.</p>

<p>It also says an agent shouldn’t keep running if its owner or boundaries are unclear. That’s a stronger line than most companies hold today.</p>

<p><strong>Test it:</strong> pick five agents in production and ask who owns each one. If the answer is the engineer who built it, or “the platform team,” the business has handed its decision to the people who wrote the code.</p>

<h3 id="2-do-its-actual-capabilities-still-match-what-was-approved">2. Do its actual capabilities still match what was approved?</h3>

<p>The paper’s answer to prompt injection and scope creep is that prompts are not controls. Critical controls “should not depend solely on a model’s ability to interpret and follow instructions.” Instead it describes a <em>harness</em>: tools, permissions, data access and limits defined in advance, enforced technically, version-controlled and changed only through change management.</p>

<p>The key line for auditors: an agent should not be able to extend its own authority, acquire new tools, create credentials or modify its own controls at runtime. High-impact actions get deterministic checks, ordinary code that verifies the action is within limits and going to an authorized recipient. If a check fails, the action stops or goes to a person. It isn’t left to the agent to sort out.</p>

<p><strong>Test it:</strong> for one agent, compare the tools and permissions it was approved for with what its credentials can actually do today. That’s an access review, and the gap between the two lists is the finding. I built <a href="https://github.com/Mihirajmera/agent-access-review">agent-access-review</a> to do this for MCP servers, and the gap is rarely zero.</p>

<h3 id="3-is-the-human-approval-real">3. Is the human approval real?</h3>

<p>This is my favorite section. “A formal approval step is not, by itself, meaningful human oversight.” Approval only counts if the approver sees the substance of the action, has the authority and time to reject it, and treats rejection as a legitimate outcome. If agents act faster or in greater volume than a person can meaningfully review, the bank shouldn’t count the approval step as a control at all.</p>

<p>That describes most approval prompts I’ve seen in agent tooling: a button clicked hundreds of times a day.</p>

<p><strong>Test it:</strong> pull the approval log for one agent. Count approvals per reviewer per day, the median time to approve, and how many were rejected. A rejection rate of zero with a median of four seconds isn’t oversight.</p>

<h3 id="4-can-you-actually-stop-it">4. Can you actually stop it?</h3>

<p>Every governance framework mentions a kill switch. This paper asks whether it works. Testing should cover whether the stop takes effect fast enough, what happens to actions already in flight, and whether the business can keep running on the fallback. It should also work “regardless of the status of a vendor.”</p>

<p>The practical test is the strongest sentence in the paper: if a bank cannot stop or constrain an agent “without unacceptable disruption, the principle is not met in substance.”</p>

<p><strong>Test it:</strong> ask when the stop mechanism was last exercised in production, how long it took, and who decided. If it has never been run, it is a design document, not a control.</p>

<h3 id="5-can-you-explain-any-action-it-took">5. Can you explain any action it took?</h3>

<p>“When software acts, the log replaces the memory and signature of a human actor.” So the paper wants every deployment recorded before production: owners, capabilities, data leaving the firm, dependencies, stop mechanism, log locations, change and suspension history.</p>

<p>Two details I hadn’t seen in a regulator’s text before. Each agent should have <strong>its own identity and credentials</strong>, because shared or borrowed accounts break the link between an action and the authority behind it. That’s the failure in the <a href="/blog/the-banks-just-ran-an-access-review-on-ai-agents/">bank agents story</a> last week, where agents logged in with customers’ passwords. And when an agent is <strong>retired</strong>, its record should be closed properly, because “casually decommissioned deployments may leave active credentials” and access behind.</p>

<p><strong>Test it:</strong> pick one action an agent took last month and trace it: which agent, which credential, which instruction, which person’s authority, which approval. Then pick one agent that was switched off and check whether its credentials still work.</p>

<h2 id="five-incidents-worth-reporting">Five incidents worth reporting</h2>

<p>The paper also asks whether these should ordinarily count as incidents under existing reporting rules:</p>

<ol>
  <li>The agent acts beyond its authority.</li>
  <li>The stop mechanism or the fallback fails when it’s needed.</li>
  <li>The agent’s identity or credentials are compromised.</li>
  <li>A monitoring agent is found to have failed silently.</li>
  <li>An approval gate lets an action through that it shouldn’t have.</li>
</ol>

<p>Most incident policies I’ve read wouldn’t classify any of these, because nothing was breached and no data was lost. Number 3 isn’t hypothetical either: last month’s <a href="/blog/nine-platforms-one-target-your-ai-keys/">run of AI platform breaches</a> was largely about stolen credentials that agents and applications use.</p>

<p><strong>Test it:</strong> take that list to your incident classification policy. For each item, would it be logged, triaged and reported, or would it be closed as a bug?</p>

<h2 id="why-this-matters-outside-hong-kong">Why this matters outside Hong Kong</h2>

<p>Hong Kong is not the only regulator thinking about this. The UK ICO’s call for evidence on agentic AI closes November 20, and NIST has an AI agent standards initiative under way. But this paper goes further than anything I’ve seen in turning principles into checks, and the checks work in any company that lets software act on its behalf.</p>

<p>If you run AI agents in production, you don’t need to wait for a rule. Run the five tests above against your most important agent this month.</p>

<h2 id="what-i-havent-covered">What I haven’t covered</h2>

<p>This is a summary of a 57-page paper that is explicitly preliminary. I’ve left out its sections on customer-facing agents, the proposed notification and registry arrangements for material deployments, and “Know Your Agent” checks for agents arriving from outside the bank, each of which deserves its own post. Read the paper itself before relying on anything here.</p>

<p><em>Sources: <a href="https://www.hkma.gov.hk/media/eng/doc/key-information/press-release/2026/20261102e3a1.pdf">HKMA discussion paper (PDF)</a> · <a href="https://www.hkma.gov.hk/eng/news-and-media/press-releases/2026/11/20261102-4/">HKMA press release</a> · <a href="https://www.caixinglobal.com/2026-11-02/hong-kong-launches-ai-agent-governance-project-102500128.html">Caixin Global</a>.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI governance" /><category term="AI agents" /><category term="Regulation" /><summary type="html"><![CDATA[Hong Kong's banking regulator published a discussion paper on governing AI agents in banks. It creates no new rules, but almost every section ends with a "practical test" an auditor can run tomorrow. Here are the tests, and what they mean outside banking.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/a-regulator-just-wrote-the-audit-program-for-ai-agents.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/a-regulator-just-wrote-the-audit-program-for-ai-agents.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Nine platforms, one target: the AI keys you handed them</title><link href="https://mihirajmera.github.io/blog/nine-platforms-one-target-your-ai-keys/" rel="alternate" type="text/html" title="Nine platforms, one target: the AI keys you handed them" /><published>2026-11-02T00:00:00-05:00</published><updated>2026-11-02T00:00:00-05:00</updated><id>https://mihirajmera.github.io/blog/nine-platforms-one-target-your-ai-keys</id><content type="html" xml:base="https://mihirajmera.github.io/blog/nine-platforms-one-target-your-ai-keys/"><![CDATA[<p>On Friday, Northflank’s status page told customers to “assume all environment variables have been decrypted” and to rotate every key, after Wiz alerted it to an attack that morning. Within hours, customers were reporting that their AI API keys were being used by someone else.</p>

<p>Northflank was the ninth. Wiz’s write-up, <a href="https://www.wiz.io/blog/black-october-for-ai-developer-platforms">Black October for AI Developer Platforms</a>, counts nine breaches of platforms that developers use to build and run AI applications, all in one month. Wiz is clear that “no single vulnerability or dependency” links them. The pattern is in what the attackers wanted: stored environment variables, especially the ones that grant access to AI models.</p>

<div class="figure-row"><div class="red"><b>9</b><span>AI developer platforms breached in October, by Wiz's count</span></div><div class="red"><b>30 min</b><span>from reported credential access to the first model call on a stolen key</span></div><div class="amber"><b>$100k+</b><span>of model usage Wiz estimates from just two stolen enterprise keys</span></div></div>

<h2 id="the-month-in-one-table">The month, in one table</h2>

<p>From Wiz’s summary, with each company’s own description of the cause where it gave one:</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Platform</th>
      <th>Cause, as described</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Oct 6</td>
      <td>Fireworks AI</td>
      <td>Credential access</td>
    </tr>
    <tr>
      <td>Oct 9</td>
      <td>Andromeda</td>
      <td>Part of an “industry-wide attack”</td>
    </tr>
    <tr>
      <td>Oct 14</td>
      <td>Zeabur</td>
      <td>Credential access; the company is shutting down</td>
    </tr>
    <tr>
      <td>Oct 15</td>
      <td>Lightning AI</td>
      <td>Credential access</td>
    </tr>
    <tr>
      <td>Oct 16–17</td>
      <td>OpenCode</td>
      <td>A “legacy system”</td>
    </tr>
    <tr>
      <td>Oct 20</td>
      <td>Railway</td>
      <td>“Privilege escalation in our build infrastructure”</td>
    </tr>
    <tr>
      <td>Oct 23</td>
      <td>Warp</td>
      <td>A third-party vendor</td>
    </tr>
    <tr>
      <td>Oct 28</td>
      <td>CommandCode</td>
      <td>Not described</td>
    </tr>
    <tr>
      <td>Oct 30</td>
      <td>Northflank</td>
      <td>Not yet disclosed</td>
    </tr>
  </tbody>
</table>

<p>This isn’t one bug to patch. It’s a category of vendor, the platform that holds your secrets so your code can run, being worked through one at a time.</p>

<h2 id="what-happens-to-a-stolen-ai-key">What happens to a stolen AI key</h2>

<p>Wiz traced two enterprise keys that were stolen in these breaches and then abused on Amazon Bedrock:</p>

<ul>
  <li>In both cases, the first model call came <strong>within 30 minutes</strong> of the reported credential access.</li>
  <li><strong>Case A:</strong> about 800,000 calls over roughly 30 hours; three quarters of the successful calls went to Opus models. The key’s policy allowed global cross-region inference, and Wiz says only about 6% of calls were throttled.</li>
  <li><strong>Case B:</strong> about 1.7 million calls over 13 hours from a single address. This key was limited to US inference profiles, so calls outside that were throttled.</li>
  <li>Together, Wiz estimates they “likely ran up over $100k in usage” at list price.</li>
</ul>

<p>Wiz also linked the key testing to services that resell access to AI models. A stolen AI key is easy to turn into money, which is why these keys were the target.</p>

<h2 id="four-controls-on-your-side-of-the-breach">Four controls on your side of the breach</h2>

<p>You can’t stop your hosting provider being breached. You can decide what it was holding, how long it stays useful, and how quickly you notice.</p>

<h3 id="1-know-which-vendors-hold-your-keys">1. Know which vendors hold your keys</h3>

<p>Most of these platforms are adopted the same way: a developer signs up with a company card, connects the repo, and pastes production keys into the environment settings. The vendor never goes through procurement, so it never appears in the vendor inventory, and nobody receives the breach notice.</p>

<p><strong>Test it:</strong> pick your three most-used AI provider keys (Anthropic, OpenAI, Bedrock or whichever you use). For each one, list every place it is stored. Then check how many of those places are in your vendor inventory. If you can’t answer the first question, the second one doesn’t matter yet.</p>

<h3 id="2-limit-what-a-stolen-key-can-do">2. Limit what a stolen key can do</h3>

<p>The two Bedrock cases are the clearest lesson in the report. The same kind of key, stolen the same way, but the one with the broader policy let the attacker spread across regions with little throttling.</p>

<p>Least privilege applies to AI keys like any other credential: restrict the models, the regions and the spend, and give each platform its own key rather than one shared key everywhere. Better still, use short-lived credentials through workload identity federation where the platform supports it, so there is no long-lived secret for a breach to take.</p>

<p><strong>Test it:</strong> for each AI key stored on a third-party platform, write down which models and regions it can use, and what its spending limit is. A key with no limits is a blank check held by your vendor.</p>

<h3 id="3-detect-use-not-just-disclosure">3. Detect use, not just disclosure</h3>

<p>Thirty minutes from theft to first use means the vendor’s status page is not your alert. Northflank’s first instruction to rotate came about two hours after Wiz’s alert, and that was a fast response.</p>

<p>The signal you control is usage: model calls from new addresses, a jump in volume, calls to models your application never uses. For most teams the first place that shows up is the bill.</p>

<p><strong>Test it:</strong> if one of your AI keys made 800,000 calls overnight, who would be told, and when? If the answer is “finance, at month end,” make spend and usage alerts a security control with an owner, not a billing setting.</p>

<h3 id="4-rotation-is-the-control-youll-need-so-practice-it">4. Rotation is the control you’ll need, so practice it</h3>

<p>Every platform in the table gave the same advice: rotate everything. Northflank had to ship rotation for its own database add-ons, one type at a time, over two days.</p>

<p>This is the same lesson as <a href="/blog/deleting-the-commit-didnt-revoke-the-key/">the LiteLLM key</a> and <a href="/blog/could-you-name-the-laptops-that-installed-it/">last week’s extension backdoor</a>. The incident is a rotation exercise, run under pressure, with whatever process you already have.</p>

<p><strong>Test it:</strong> time a full rotation of the secrets on one platform: revoke at the provider, issue new ones, update the workloads, confirm the old ones fail. If it takes days, that’s how long a stolen key keeps working.</p>

<h2 id="what-to-ask-these-vendors">What to ask these vendors</h2>

<p>For any platform that stores secrets for you:</p>

<ol>
  <li>Who and what can decrypt our environment variables, and where are the keys held?</li>
  <li>Can we reference secrets from our own secret manager, or use short-lived federated credentials, instead of storing them with you?</li>
  <li>Do you offer audit logs of secret access? (Northflank turned them on for all customers <em>after</em> the breach.)</li>
  <li>How fast will you tell us, and will you tell us which of our secrets were exposed?</li>
</ol>

<h2 id="what-i-havent-verified">What I haven’t verified</h2>

<p>Wiz’s list relies on each company’s public statements, and several causes are still preliminary. Northflank hasn’t published a root cause yet; its latest update, on November 2, says it believes the attack was contained within an hour, that running workloads weren’t accessed, and that a root cause analysis will follow. I haven’t seen any of the investigations directly.</p>

<p><em>Sources: <a href="https://www.wiz.io/blog/black-october-for-ai-developer-platforms">Wiz, Black October for AI Developer Platforms</a> · <a href="https://status.northflank.com/cmvv0rn5a092f14rutwccodpp">Northflank status page</a> · <a href="https://www.bleepingcomputer.com/news/security/northflank-security-breach-leads-to-abuse-of-customers-ai-api-keys/">BleepingComputer</a>.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="Third-party risk" /><category term="Secrets management" /><category term="AI supply chain" /><summary type="html"><![CDATA[Wiz counts nine breaches of AI developer platforms in October, ending with Northflank telling customers to assume every environment variable was decrypted. There's no single vulnerability behind them. The common thread is what attackers came for, and the controls that decide how much it costs you are on your side, not the vendor's.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/nine-platforms-one-target-your-ai-keys.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/nine-platforms-one-target-your-ai-keys.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Could you name the laptops that installed it? The Claude Code extension backdoor, read as an auditor</title><link href="https://mihirajmera.github.io/blog/could-you-name-the-laptops-that-installed-it/" rel="alternate" type="text/html" title="Could you name the laptops that installed it? The Claude Code extension backdoor, read as an auditor" /><published>2026-10-30T00:00:00-04:00</published><updated>2026-10-30T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/could-you-name-the-laptops-that-installed-it</id><content type="html" xml:base="https://mihirajmera.github.io/blog/could-you-name-the-laptops-that-installed-it/"><![CDATA[<p>On Wednesday I wrote that in the LiteLLM attack, <a href="/blog/deleting-the-commit-didnt-revoke-the-key/">deleting the commit didn’t revoke the key</a>. A day later the same lesson came back from a different direction: uninstalling the extension doesn’t remove the backdoor.</p>

<p>On October 29, from about 2:27am to 1:35pm Pacific time, Cursor’s VS Code extension marketplace served a maliciously modified build of the Claude Code extension, version 2.1.313. Cursor’s <a href="https://cursor.com/blog/claude-code-extension-security-notice">security notice</a> says the build “was not developed by Anthropic” and that at most 5,000 users were affected. Version 2.1.314 is clean.</p>

<div class="figure-row"><div class="amber"><b>11h</b><span>the tampered build was available on Cursor's marketplace</span></div><div class="red"><b>5,000</b><span>users at most received it, according to Cursor</span></div><div class="red"><b>1</b><span>SSH key, labeled svc-telemetry, that stays behind after you uninstall</span></div></div>

<h2 id="what-the-build-did">What the build did</h2>

<p>From Cursor’s notice and <a href="https://www.stepsecurity.io/blog/claude-code-extension-trojanized">StepSecurity’s analysis</a>:</p>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>What it did</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Recon</td>
      <td>Collected hostname, username, OS, home directory, working directories, Git email, public IP, and the <em>names</em> of environment variables. StepSecurity says it looked for names containing KEY, TOKEN, SECRET, PASS, AWS or GCP.</td>
    </tr>
    <tr>
      <td>Access</td>
      <td>Added an attacker-controlled key to <code class="language-plaintext highlighter-rouge">authorized_keys</code>, labeled <code class="language-plaintext highlighter-rouge">svc-telemetry</code>, and tried to turn on the SSH service and open a reverse tunnel.</td>
    </tr>
    <tr>
      <td>Persistence</td>
      <td>A LaunchAgent named <code class="language-plaintext highlighter-rouge">com.apple.sysmond</code> on macOS; a scheduled task named <code class="language-plaintext highlighter-rouge">SysMon</code>, running as SYSTEM, on Windows. Scripts retried the tunnel about every 60 seconds.</td>
    </tr>
    <tr>
      <td>Report</td>
      <td>Sent host details and backdoor status to the attacker’s server over plain HTTP.</td>
    </tr>
  </tbody>
</table>

<p>The impact was probably smaller than that list suggests. Turning on the SSH service needs admin rights, which the extension usually didn’t have, so the tunnel usually failed. The attacker’s server is now offline. But the key was still added and the persistence still installed, and Cursor says plainly that “uninstalling the extension may not be sufficient.”</p>

<p>Nothing here is new technique. What makes it worth an auditor’s time is <em>where</em> it landed: the machine where your engineers hold their cloud credentials, Git tokens and production access.</p>

<h2 id="four-control-questions-and-the-test-for-each">Four control questions, and the test for each</h2>

<h3 id="1-could-you-name-the-laptops-that-installed-it">1. Could you name the laptops that installed it?</h3>

<p>Cursor’s first instruction is to find everyone who installed or updated to 2.1.313 during the window. That should be a five-minute query. In most companies I’ve looked at, it’s a Slack message asking people to check.</p>

<p>Asset inventories usually stop at the device. They know the laptop exists and which OS it runs. They rarely know which editor extensions are installed, at which version, which is exactly what an incident like this needs.</p>

<p><strong>Test it:</strong> pick one extension your developers use, and ask how long it takes to produce a list of every machine running a specific version of it. If the answer involves asking people, extensions aren’t in your software inventory, and an AI coding extension runs with the same rights as the developer.</p>

<h3 id="2-updates-arrive-faster-than-anyone-can-review-them">2. Updates arrive faster than anyone can review them</h3>

<p>Editors built on VS Code update extensions automatically in most setups. The bad build was live for about eleven hours. Anyone whose editor checked for updates in that window got it without doing anything.</p>

<p>You can’t review every extension update, and you shouldn’t try. The cheaper control is time. Package managers already support a minimum release age (pnpm and Renovate both have one): don’t install a version until it has been public for a set period. Most malicious releases are caught and pulled within a day or two. A 48-hour rule would have skipped this build entirely, and the <a href="/blog/the-ai-gateway-is-a-crown-jewel/">LiteLLM wheels</a> too, which were live for about two hours.</p>

<p>VS Code also has an allowlist setting, <code class="language-plaintext highlighter-rouge">extensions.allowed</code>, that can restrict which extensions and versions are installed. Check whether your editors honor it before relying on it.</p>

<p><strong>Test it:</strong> for your developer tooling (extensions, package managers, CLI plugins), write down who decides when an update is installed. If the answer is “the publisher, automatically,” you have outsourced change management to whoever controls the publisher’s account.</p>

<h3 id="3-do-you-know-what-to-rotate">3. Do you know what to rotate?</h3>

<p>Cursor’s notice says to rotate “credentials and sessions accessible from affected hosts,” and warns that the limited collection “does not rule out later credential access over the backdoor.”</p>

<p>That instruction assumes you know which credentials are on a developer’s laptop. Usually nobody does: long-lived cloud keys in <code class="language-plaintext highlighter-rouge">~/.aws/credentials</code>, a GitHub token in the CLI config, <code class="language-plaintext highlighter-rouge">.env</code> files in a dozen old project folders, a database password in shell history.</p>

<p>The recon step only collected variable <em>names</em>. That’s a target list for a second visit.</p>

<p><strong>Test it:</strong> pick one developer laptop and list every credential on it that grants access to something real. Then check how many are long-lived. Every long-lived credential on that list is something you’d have to rotate by hand after an incident like this one, and something that keeps working if you miss it.</p>

<h3 id="4-uninstalling-isnt-remediation">4. Uninstalling isn’t remediation</h3>

<p>Removing the extension leaves the SSH key, the scheduled task and the LaunchAgent in place. A ticket that closes when the bad version is gone has closed the wrong thing. This is the same gap as a leaked secret that is deleted from the repo but never revoked.</p>

<p>The persistence was also built to blend in. <code class="language-plaintext highlighter-rouge">SysMon</code> and <code class="language-plaintext highlighter-rouge">com.apple.sysmond</code> look like system components at a glance, and <code class="language-plaintext highlighter-rouge">svc-telemetry</code> looks like a service account.</p>

<p><strong>Test it:</strong> does your endpoint tooling alert on a new entry in <code class="language-plaintext highlighter-rouge">authorized_keys</code> on a laptop, a new scheduled task running as SYSTEM, or a new LaunchAgent? Developer machines are often given looser rules because developers install things all day. Check whether that exception covers exactly these events.</p>

<h2 id="what-to-ask-your-vendors">What to ask your vendors</h2>

<p>If your developers use an AI-enabled editor, add these to the vendor review:</p>

<ol>
  <li>How does the marketplace verify that a package matches what the named publisher actually built?</li>
  <li>If a publisher’s account is compromised, what stops a new version reaching every user within the hour?</li>
  <li>Can we control which versions are installed, and delay updates by a set period?</li>
  <li>How quickly will you tell us which of our users installed a bad version?</li>
</ol>

<h2 id="what-i-havent-verified">What I haven’t verified</h2>

<p>The details above come from Cursor’s notice and StepSecurity’s analysis. Neither says how the modified package got into the marketplace, and they describe the channel differently: Cursor’s notice is about its own marketplace, while StepSecurity says the build was also published to Open VSX, under the legitimate Anthropic namespace. I haven’t seen the package or the logs, and the account may change as the investigation goes on.</p>

<p>If you or your team installed Claude Code from Cursor’s marketplace on October 29, follow <a href="https://cursor.com/blog/claude-code-extension-security-notice">Cursor’s notice</a> first. It has the indicators and the cleanup scripts.</p>

<p><em>Sources: <a href="https://cursor.com/blog/claude-code-extension-security-notice">Cursor security notice</a> · <a href="https://www.stepsecurity.io/blog/claude-code-extension-trojanized">StepSecurity</a> · <a href="https://www.helpnetsecurity.com/2026/10/30/claude-code-extension-ssh-backdoor/">Help Net Security</a> · <a href="https://x.com/cursor_ai/status/2116074289053175808">Cursor on X</a>.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI supply chain" /><category term="Developer endpoints" /><category term="Incident analysis" /><summary type="html"><![CDATA[For about eleven hours on October 29, Cursor's extension marketplace served a tampered build of the Claude Code extension that planted an SSH key and persistence on developer machines. The payload is ordinary. The control questions it raises about developer endpoints are not, and most teams can't answer the first one.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/could-you-name-the-laptops-that-installed-it.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/could-you-name-the-laptops-that-installed-it.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Deleting the commit didn’t revoke the key: the LiteLLM root cause, read as an auditor</title><link href="https://mihirajmera.github.io/blog/deleting-the-commit-didnt-revoke-the-key/" rel="alternate" type="text/html" title="Deleting the commit didn’t revoke the key: the LiteLLM root cause, read as an auditor" /><published>2026-10-28T00:00:00-04:00</published><updated>2026-10-28T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/deleting-the-commit-didnt-revoke-the-key</id><content type="html" xml:base="https://mihirajmera.github.io/blog/deleting-the-commit-didnt-revoke-the-key/"><![CDATA[<p>Last week I wrote about <a href="/blog/the-ai-gateway-is-a-crown-jewel/">the LiteLLM release compromise</a>, when version 1.107.0 shipped a credential stealer through LiteLLM’s real release pipeline. At the time, LiteLLM’s working theory was that the attacker took GitHub tokens from an internal deployment’s logs, and my post repeated that.</p>

<p>On October 23 LiteLLM published <a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026-attack-chain">the full attack chain</a>, and the log theory was wrong. The real story is simpler, and more useful for anyone who runs or audits controls. Every step is an ordinary control that existed on paper.</p>

<div class="figure-row"><div class="red"><b>136</b><span>days the leaked master key stayed valid before it was used</span></div><div class="amber"><b>11</b><span>days between the first break-in and the poisoned release</span></div><div class="red"><b>1</b><span>org owner's personal GitHub token, sitting in an AI agent's config</span></div></div>

<h2 id="what-actually-happened">What actually happened</h2>

<p>All from LiteLLM’s post; times are UTC.</p>

<table>
  <thead>
    <tr>
      <th>When</th>
      <th>What</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>May 27</td>
      <td>A config file containing the master key for LiteLLM’s internal Agent Platform is committed to a public repo. The commit is force-pushed over. The key is not revoked.</td>
    </tr>
    <tr>
      <td>Oct 10, 21:27</td>
      <td>The attacker uses that key to call the Agent Platform API. One request returns the configuration of every managed agent, including a GitHub CLI token belonging to one of the two GitHub organization owners.</td>
    </tr>
    <tr>
      <td>Oct 10, 22:11</td>
      <td>Using the owner’s access, the attacker runs a workflow on a throwaway branch that posts six secrets, including a second maintainer’s token, to an outside endpoint. A second workflow takes a third maintainer’s token. Both runs are deleted in under three minutes.</td>
    </tr>
    <tr>
      <td>Oct 12</td>
      <td>A second round of exfiltration across private repos.</td>
    </tr>
    <tr>
      <td>Oct 21, 10:08</td>
      <td>A push to the release workflow is rejected by branch protection, then forced through five seconds later with the owner’s token.</td>
    </tr>
    <tr>
      <td>Oct 21, 11:11</td>
      <td>A second stolen maintainer token approves the production release gate.</td>
    </tr>
    <tr>
      <td>Oct 21, 11:39</td>
      <td>The poisoned wheels go to PyPI. They are live for about two hours.</td>
    </tr>
  </tbody>
</table>

<h2 id="four-control-failures-and-the-test-for-each">Four control failures, and the test for each</h2>

<h3 id="1-a-leaked-secret-was-deleted-not-revoked">1. A leaked secret was deleted, not revoked</h3>

<p>Rewriting git history removes a commit from the branch. It does nothing about copies that were already made, and it doesn’t make the key stop working. The key stayed valid for 136 days.</p>

<p>This is the most common gap I see in secrets handling: the ticket closes when the secret disappears from the repo, because that’s what the scanner checks.</p>

<p><strong>Test it:</strong> pull your last ten secret-scanning alerts marked resolved. For each one, is there evidence the credential was <em>revoked</em>, such as a rotation record or the old key returning 401? “Removed from code” is not remediation.</p>

<h3 id="2-an-ai-agent-ran-on-an-org-owners-personal-token">2. An AI agent ran on an org owner’s personal token</h3>

<p>The token in the agent’s config was, in LiteLLM’s words, “not associated with a service account and was not scoped down.” It belonged to one of the two people with the most power in the GitHub organization. And the platform’s API returned it in full to anyone holding the master key.</p>

<p>I wrote on Monday that <a href="/blog/the-banks-just-ran-an-access-review-on-ai-agents/">an agent using your password <em>is</em> you</a> as far as every control can tell. This is the same failure inside a software company. LiteLLM also notes that these CLI tokens skip GitHub’s re-authentication prompt for sensitive actions, so the strongest account was protected by the weakest credential type.</p>

<p><strong>Test it:</strong> list every token your AI agents, MCP servers and automation hold. Which ones belong to a named person, and which of those people are admins or owners? Any agent config API that returns secrets in plain text is a finding on its own.</p>

<h3 id="3-a-control-was-relaxed-to-ship-faster-and-nobody-owned-the-risk">3. A control was relaxed to ship faster, and nobody owned the risk</h3>

<p>Releases used to need approval from “a special account locked to a physical device in our office.” That was relaxed “earlier this week” to allow faster releases: any two engineers could trigger and approve.</p>

<p>Two-person approval only works if the two credentials can’t be stolen together. A device-locked approver can’t be replayed from a stolen token; two engineers’ CLI tokens can. Weakening the control turned a two-person rule into a two-token rule, and the attacker already had three tokens.</p>

<p>The decision to relax it was reasonable. The gap is that it wasn’t treated as what it was: a risk acceptance. That is the same pattern as <a href="/blog/decide-who-takes-it-offline-before-the-advisory/">yesterday’s post</a> on compensating controls. A temporary change to a control needs an owner, an expiry date and something that compensates while it’s in place.</p>

<p><strong>Test it:</strong> ask when your release approval, branch protection or MFA settings last changed, who approved the change, and when it is due to be reversed. If the answer comes from the audit log instead of a change record, that’s the finding.</p>

<h3 id="4-eleven-days-inside-with-the-evidence-deleted">4. Eleven days inside, with the evidence deleted</h3>

<p>The attacker was in on October 10 and released on October 21. The exfiltration workflows ran from throwaway branches and were deleted in under three minutes. On release day, branch protection was overridden and the run records were deleted.</p>

<p>Every one of those actions shows up in GitHub’s audit log: a workflow run deleted, branch protection bypassed by an owner, a new branch pushed and removed. They’re rare events, and each one was a chance to notice.</p>

<p><strong>Test it:</strong> do you alert on branch-protection bypasses, deleted workflow runs, and organization-owner tokens used from a new IP? If those events only exist in a log nobody reads, the control is evidence after the fact, not detection.</p>

<h2 id="what-to-ask-your-vendors">What to ask your vendors</h2>

<p>LiteLLM reports holding <a href="https://docs.litellm.ai/blog/soc2-type-2-report">SOC 2 Type 2</a> and <a href="https://docs.litellm.ai/blog/vanta-compliance-recertification">ISO 27001</a>. I’m not suggesting the audits were wrong; I haven’t seen the reports, and an audit covers a period and a scope. The point is that none of the four failures above would show up unless someone asked about them directly. For any vendor in your AI supply chain, add these to the questionnaire:</p>

<ol>
  <li>When a secret is exposed, what evidence shows it was revoked, not just removed?</li>
  <li>Do automated agents or pipelines ever run on a named person’s credentials?</li>
  <li>Is release approval bound to something that can’t be replayed, like a hardware key?</li>
  <li>How are temporary relaxations of security controls approved and tracked?</li>
</ol>

<h2 id="what-i-havent-verified">What I haven’t verified</h2>

<p>Everything above comes from LiteLLM’s own account, and it says the process changes will be published separately. I haven’t seen the audit logs or the agent platform. LiteLLM corrected its own first account, and details may change again.</p>

<p>Credit to LiteLLM for publishing the attack chain in this much detail, including the parts where its first theory was wrong. Most companies never publish that.</p>

<p><em>Sources: <a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026-attack-chain">LiteLLM, The attack chain behind the v1.107.0 release</a> · <a href="https://thehackernews.com/2026/10/leaked-litellm-master-key-let-attackers.html">The Hacker News</a> · <a href="https://www.wiz.io/blog/litellm-compromised-steals-credentials">Wiz</a>.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI supply chain" /><category term="Secrets management" /><category term="Incident analysis" /><summary type="html"><![CDATA[LiteLLM has published the real root cause of the October 21 release compromise. A master key leaked in May, the commit was rewritten but the key was never revoked, and 136 days later it unlocked an agent platform holding an org owner's GitHub token. Four control failures, and what to test for each.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/deleting-the-commit-didnt-revoke-the-key.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/deleting-the-commit-didnt-revoke-the-key.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Decide who takes it offline before the advisory lands</title><link href="https://mihirajmera.github.io/blog/decide-who-takes-it-offline-before-the-advisory/" rel="alternate" type="text/html" title="Decide who takes it offline before the advisory lands" /><published>2026-10-27T00:00:00-04:00</published><updated>2026-10-27T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/decide-who-takes-it-offline-before-the-advisory</id><content type="html" xml:base="https://mihirajmera.github.io/blog/decide-who-takes-it-offline-before-the-advisory/"><![CDATA[<p>Last week I wrote that <a href="/blog/your-patch-sla-was-written-for-a-slower-attacker/">your patch SLA was written for a slower attacker</a>. The replies were better than the post. One reader argued for moving from checkbox compliance to continuous, risk-based exposure management. Answering them made me realise the real gap wasn’t the SLA. It was the three decisions nobody had made in advance.</p>

<h2 id="what-the-week-looked-like">What the week looked like</h2>

<p>A quick recap, all from the sources in the earlier post:</p>

<ul>
  <li><strong>Oct 17:</strong> Amazon’s honeypots saw PHP2Shell exploitation, <a href="https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/">four days before public disclosure</a>.</li>
  <li><strong>Oct 21:</strong> details leaked and PHP shipped fixed releases the same day (<a href="https://www.wiz.io/blog/php2shell-php-rce">Wiz</a>).</li>
  <li><strong>Oct 21–22:</strong> the FBI advised taking sites that couldn’t be patched offline, and some government sites in the UK, Finland and Japan went partly offline (<a href="https://www.recordedfuture.com/blog/php2shell-vulnerability-exploitation">Recorded Future</a>).</li>
  <li><strong>Oct 22:</strong> Palo Alto said every PAN-OS version was affected with no fix yet, and Fortinet and Cisco had dozens of products under investigation (<a href="https://www.helpnetsecurity.com/2026/10/22/php2shell-vulnerability-fortinet-cisco-palo-alto-networks/">Help Net Security</a>).</li>
</ul>

<p>Two things in that list can’t be fixed by patching faster. Exploitation started before anyone could patch, and for some appliances there was nothing to patch with.</p>

<h2 id="decision-1-who-can-take-it-offline">Decision 1: who can take it offline</h2>

<p>When the FBI says “take it offline,” someone has to decide that for each system. In most companies that person isn’t written down anywhere.</p>

<p>An outage runbook usually covers the service breaking. It rarely covers choosing to break it yourself. So the call gets made in a meeting, under pressure, by whoever is senior enough and awake. Then comes a second round of delay: legal, comms and the business owner each find out separately.</p>

<p>Write it down per system before you need it:</p>

<ul>
  <li><strong>Who can approve taking it offline.</strong> One name, plus a deputy, both reachable out of hours.</li>
  <li><strong>Who must be told.</strong> The business owner, the customer-facing teams, and legal if contracts have uptime terms.</li>
  <li><strong>What customers hear.</strong> A pre-approved holding message, so comms isn’t drafting from scratch at 2 a.m.</li>
  <li><strong>The longest acceptable outage.</strong> If it’s two hours, you already know an all-day shutdown needs a different answer.</li>
</ul>

<p>This is a risk-acceptance decision made in advance, which is exactly what GRC teams are meant to own.</p>

<h2 id="decision-2-which-vendors-embed-the-runtime">Decision 2: which vendors embed the runtime</h2>

<p>“Patch PHP” assumes you own the PHP. On a firewall, VPN or appliance, the vendor owns it and you wait.</p>

<p>What you can control is how fast you ask. Keep a list of which vendor products embed common runtimes and libraries: PHP, OpenSSL, log4j, glibc. Your SBOM requests and vendor questionnaires should already hold most of this. When the advisory lands, the email to each affected vendor goes out the same day, asking whether they’re affected, when a fix is coming, and what mitigation to use until then.</p>

<p>A vendor that can’t answer within a day is telling you something too. Record it in your third-party risk file, because next renewal is the time to raise it.</p>

<h2 id="decision-3-the-clock-starts-at-disclosure">Decision 3: the clock starts at disclosure</h2>

<p>Most patch SLAs start counting when a patch is available. PHP2Shell shows why that’s the wrong starting point. For PAN-OS there was no patch, so the clock never started, while attackers were already working.</p>

<p>Start the clock at disclosure. The first deadline isn’t “patched.” It’s “a compensating control in place within hours.” That could be a WAF rule, restricting access to the management interface, or taking the service offline (see decision 1). Each compensating control needs three things:</p>

<ul>
  <li><strong>An owner</strong>, so someone is accountable for removing it.</li>
  <li><strong>An expiry</strong>, so a temporary rule doesn’t quietly become permanent.</li>
  <li><strong>Proof it works.</strong> Send a safe test request and confirm it’s blocked. An untested WAF rule is a guess.</li>
</ul>

<p>Then look back. Amazon saw exploitation four days before disclosure. Patching on day one doesn’t tell you whether someone got in on day minus four. Search your logs from before the disclosure date, not just after.</p>

<h2 id="a-one-row-template">A one-row template</h2>

<p>Copy this per internet-facing system. It takes about ten minutes each and makes the next bad week shorter.</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>System</td>
      <td>Customer portal (<code class="language-plaintext highlighter-rouge">portal.example.com</code>)</td>
    </tr>
    <tr>
      <td>Can approve taking it offline</td>
      <td>Head of Platform; deputy: on-call engineering manager</td>
    </tr>
    <tr>
      <td>Must be told</td>
      <td>Product owner, support lead, legal (SLA terms)</td>
    </tr>
    <tr>
      <td>Customer message</td>
      <td>Pre-approved status-page text, stored in the comms runbook</td>
    </tr>
    <tr>
      <td>Longest acceptable outage</td>
      <td>4 hours</td>
    </tr>
    <tr>
      <td>Embedded runtimes</td>
      <td>PHP 8.3 (app); OpenSSL via load balancer appliance</td>
    </tr>
    <tr>
      <td>Vendor contact for advisories</td>
      <td>Appliance vendor PSIRT address and account manager</td>
    </tr>
    <tr>
      <td>Default compensating controls</td>
      <td>WAF virtual patch; restrict admin paths to VPN</td>
    </tr>
    <tr>
      <td>Log retention for look-back</td>
      <td>90 days in SIEM</td>
    </tr>
  </tbody>
</table>

<h2 id="what-i-havent-tested">What I haven’t tested</h2>

<p>This is a proposal, not a measured result. I haven’t run this template through a real incident, and the right maximum outage and approvers will differ by company. The FBI advice and government shutdowns are as reported by Recorded Future; I haven’t independently confirmed which sites went offline or for how long.</p>

<p>Thanks to the readers whose comments on the last post shaped this one.</p>]]></content><author><name>Mihir Ajmera</name></author><category term="Vulnerability management" /><category term="Incident response" /><category term="Third-party risk" /><summary type="html"><![CDATA[PHP2Shell was exploited four days before disclosure, and some vendors still had no fix a day after it. The decisions that mattered that week can't be made well in the moment. Here are three to write down per system now, with a one-row template.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/decide-who-takes-it-offline-before-the-advisory.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/decide-who-takes-it-offline-before-the-advisory.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The banks just ran an access review on AI agents. Here’s what they found.</title><link href="https://mihirajmera.github.io/blog/the-banks-just-ran-an-access-review-on-ai-agents/" rel="alternate" type="text/html" title="The banks just ran an access review on AI agents. Here’s what they found." /><published>2026-10-26T00:00:00-04:00</published><updated>2026-10-26T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/the-banks-just-ran-an-access-review-on-ai-agents</id><content type="html" xml:base="https://mihirajmera.github.io/blog/the-banks-just-ran-an-access-review-on-ai-agents/"><![CDATA[<p>Over the past week, the largest banks in the United States have quietly started turning AI agents away at the door.</p>

<p>According to Reuters, Axios and the Financial Times, Morgan Stanley is blocking outside AI agents from its platforms. JPMorgan Chase, Bank of America and Citi are restricting them. Wells Fargo has rewritten its online access agreement. Charles Schwab is blocking agents that try to move client money. The reports describe agents logging into customer accounts with the customer’s own username and password, and some that, per Axios, “attempted to take actions that appeared to be independent of user instructions.”</p>

<p>It’s being covered as a story about banks resisting AI. I think it’s something more useful: <strong>the first large-scale access review of AI agents</strong>, run by the institutions with the most to lose. What they found applies to every company that lets an agent act on someone’s behalf, including yours.</p>

<div class="figure-row"><div class="amber"><b>6</b><span>major US banks and brokers now blocking or restricting outside AI agents</span></div><div class="red"><b>1</b><span>password shared by customer and agent, so the bank can't tell them apart</span></div><div class=""><b>2/3</b><span>of financial-services and software CISOs rank agentic AI a top-three risk</span></div></div>

<h2 id="what-happened">What happened</h2>

<table>
  <thead>
    <tr>
      <th>Institution</th>
      <th>Reported action</th>
      <th>Source</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Morgan Stanley</td>
      <td>Blocking third-party agents; a source said it had seen no rogue agent instances</td>
      <td>Reuters, Axios</td>
    </tr>
    <tr>
      <td>JPMorgan Chase</td>
      <td>Limiting agents’ access to client accounts and building tools to detect them</td>
      <td>Q3 earnings call, Reuters</td>
    </tr>
    <tr>
      <td>Bank of America</td>
      <td>Restricting agent access to consumer deposits; developing protocols on liability</td>
      <td>FT, Bloomberg</td>
    </tr>
    <tr>
      <td>Citi</td>
      <td>No third-party agents may log in or transact; read-only access through approved open-banking providers</td>
      <td>Citi statement</td>
    </tr>
    <tr>
      <td>Wells Fargo</td>
      <td>Steps to block unauthorized agents; updated access agreement takes effect Nov. 24</td>
      <td>USA Today</td>
    </tr>
    <tr>
      <td>Charles Schwab</td>
      <td>Blocking unauthorized agents that try to move client money</td>
      <td>Bloomberg, Reuters</td>
    </tr>
  </tbody>
</table>

<p>The quotes are telling. Jamie Dimon said JPMorgan would limit agents “because you don’t want rogue agents and there could be thousands of them.” Brian Moynihan of Bank of America said there “should be a human in the loop to authorise money moving out of your account.” Schwab’s Rick Wurster said: “We are blocking it because we want to have control over authentication and the experience.”</p>

<p>Two caveats before going further. Much of the reporting relies on anonymous sources, and none of it establishes widespread unauthorized transfers or customer losses. This is the industry reacting to a risk, not cleaning up after a disaster. Also, the banks have commercial reasons too: the FT reported a fear that agents will move idle deposits to higher-yielding accounts. Both things can be true.</p>

<h2 id="why-this-one-matters">Why this one matters</h2>

<h3 id="an-agent-with-your-password-is-you">An agent with your password <em>is</em> you</h3>

<p>Every control a bank runs downstream of login assumes one thing: <strong>the identity that authenticated is the person who acted.</strong> Fraud models are trained on that person’s behavior. Audit logs record that person’s ID. Consumer protection rules hinge on whether that person authorized the transaction.</p>

<p>When a customer gives an agent their username and password, all of that collapses. The bank sees the customer. The logs record the customer. And as Reuters put it, such transactions “cannot be deemed unauthorized if credentials were shared by the customer.”</p>

<p>In audit terms, this is a loss of <strong>attribution</strong>, and attribution is the foundation of accountability. If you can’t tell who acted, you can’t tell whether they were allowed to, you can’t investigate when something goes wrong, and you can’t assign responsibility. It’s the same reason companies ban shared admin accounts. A shared password between a person and their agent is a shared account.</p>

<h3 id="authorized-isnt-intended">“Authorized” isn’t “intended”</h3>

<p>The most consequential detail in the coverage got the least attention. USA Today reported that Wells Fargo’s updated agreement treats transactions made by a customer’s authorized agents as authorized, including ones “you did not want or intend,” until the customer notifies the bank and it has had a reasonable chance to act. Customers are described as solely responsible for supervising their agents.</p>

<p>That’s a rational move for a bank, and a clear signal of where liability is heading. For decades, “authorized” meant “the customer did it or told someone to.” Agents split that in two. The customer authorized the <em>agent</em>. Did they authorize <em>this action</em>? Probably not, if the agent “appeared to act independently of user instructions.”</p>

<p>Every company deploying agents will face the same split, and most haven’t written down where they stand. The banks have just shown what the default answer will be if you don’t: whoever granted the access owns the outcome.</p>

<h3 id="the-banks-landed-on-least-privilege">The banks landed on least privilege</h3>

<p>Strip away the AI and look at what the banks are converging on: <strong>read-only access for agents, and a human approval for anything that moves money.</strong> Citi already offers read-only access through approved open-banking providers. Moynihan wants a human in the loop on payments.</p>

<p>That’s not a new idea. It’s least privilege and segregation of duties, the two oldest controls in access management. The agent can see; a human must authorize. The agent proposes; a different identity approves. The banks didn’t need a new framework for AI. They needed to apply the old one to a new kind of identity.</p>

<h2 id="what-would-have-helped-ranked-by-cost">What would have helped, ranked by cost</h2>

<p><strong>For anyone letting agents act on behalf of people (including inside your company):</strong></p>

<ol>
  <li><strong>Never let an agent use a human’s credentials.</strong> If an agent needs access, it gets its own identity, with a token that’s scoped, expiring and revocable. Open banking already works this way: delegated, read-only tokens instead of passwords. That’s what makes Citi’s middle ground possible.</li>
  <li><strong>Make agent actions distinguishable in the logs.</strong> Every action should record both the agent and the human it acts for. Without that, an investigation can’t tell “the user did this” from “the user’s agent did this.”</li>
  <li><strong>Split read from act.</strong> Give agents read access by default. Actions that move money, delete data, send messages or change access need a separate grant, and usually a human approval.</li>
  <li><strong>Write down who owns an agent’s mistakes.</strong> Before an incident, not after. Wells Fargo did; most internal AI programs haven’t.</li>
</ol>

<p><strong>For companies offering an API or platform that agents will use:</strong></p>

<ol>
  <li><strong>Offer a sanctioned path, or people will use the unsanctioned one.</strong> Customers gave agents their passwords because there was no better option. Blocking alone pushes agents toward screen-scraping and credential sharing, which is harder to see.</li>
  <li><strong>Authenticate the human for high-risk steps with something an agent can’t replay.</strong> Ping Identity’s Andre Durand told USA Today that email one-time passwords are no longer enough, because agents often have access to email. Passkeys and biometrics bind the approval to a person.</li>
  <li><strong>Detect agents, not just attackers.</strong> JPMorgan says it is building tools to tell agents apart from customers. Behavioral signals like timing, navigation and request patterns differ between people and software, even with the same password.</li>
</ol>

<h2 id="now-look-inside-your-own-company">Now look inside your own company</h2>

<p>The banks’ problem is visible because it involves money. The same pattern is already inside most companies, just quieter.</p>

<p>An engineer gives a coding agent their personal access token. An analyst connects an assistant to their mailbox with their own session. An MCP server runs with a developer’s cloud credentials. In every case, the agent is acting under a human’s identity, and the logs, the access reviews and the incident response all see a human.</p>

<p>Ask the same questions the banks just asked:</p>

<ul>
  <li><strong>Inventory:</strong> Which agents are running in our environment, and whose credentials are they using?</li>
  <li><strong>Attribution:</strong> If an agent took an action today, would our logs show it was the agent?</li>
  <li><strong>Scope:</strong> Do agents have read-only access by default, with a separate, approved grant to act?</li>
  <li><strong>Approval:</strong> Which agent actions require a human, and is that enforced technically or just written in a policy?</li>
  <li><strong>Ownership:</strong> Who is accountable when an agent does something nobody intended?</li>
</ul>

<p>This is exactly the review I built <a href="https://github.com/Mihirajmera/agent-access-review">agent-access-review</a> to run: a user access review for AI agents and MCP servers, checking who owns each agent, what it can reach, whether it can act or only read, and whether anyone approved it. When I ran it against the <a href="/case-studies/mcp-readme-configs/">official MCP README examples</a>, 7 of 9 granted write, send, delete or execute permissions, and nothing in a copied config records who owns the agent or who approved that access.</p>

<h2 id="what-this-means-for-ai-governance">What this means for AI governance</h2>

<p>AI governance conversations tend to focus on what models say. The banks’ reaction is a reminder that the bigger risk is what agents <em>do</em>, and that the controls for that risk already exist. They were built for people, and they work only when the system knows which identity is acting.</p>

<p>Three principles fall out of this week:</p>

<ol>
  <li><strong>AI agents are identities.</strong> Give them their own credentials, review their access like any privileged user’s, and never let them borrow a human’s.</li>
  <li><strong>Authorization needs two parts now.</strong> The human authorized the agent. Something still has to authorize the action, and for anything consequential that something should be a human.</li>
  <li><strong>If you don’t decide who’s liable, your terms of service will.</strong> Wells Fargo’s agreement is a preview of how accountability will be allocated by default.</li>
</ol>

<p>The banks didn’t ban AI. They did what any good access review does: they found accounts they couldn’t attribute, access nobody had scoped, and actions nobody had approved, and they turned it off until they could. Every company with agents should run the same review before an incident runs it for them.</p>

<p><em>Sources: <a href="https://www.hawaiitribune-herald.com/2026/10/23/nation-world-news/us-banks-are-blocking-third-party-ai-agents-access-to-customer-accounts-sources-say/">Reuters via Hawaii Tribune-Herald</a> · <a href="https://finance.yahoo.com/technology/ai/articles/major-banks-rogue-ai-agents-152418947.html">Axios via Decrypt</a> · <a href="https://finance.yahoo.com/news/us-banks-limit-ai-agents-113229223.html">Financial Times via Retail Banker International</a> · <a href="https://finance.yahoo.com/technology/ai/articles/window-vulnerability-banks-move-restrict-202731426.html">USA Today</a> · <a href="https://www.ibtimes.co.uk/us-banks-restrict-third-party-ai-agents-1827385">IBTimes UK</a>. Much of the reporting relies on anonymous sources, and none of it establishes customer losses. The CISO figure is from a 2026 Palo Alto Networks study cited by USA Today.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI agents" /><category term="Identity and access" /><category term="AI governance" /><summary type="html"><![CDATA[America's biggest banks are blocking outside AI agents from customer accounts. The reason isn't that agents are dangerous. It's that an agent using your password is indistinguishable from you, and every control downstream depends on knowing who acted.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/the-banks-just-ran-an-access-review-on-ai-agents.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/the-banks-just-ran-an-access-review-on-ai-agents.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Your patch SLA was written for a slower attacker: what PHP2Shell teaches about vulnerability governance in the AI era</title><link href="https://mihirajmera.github.io/blog/your-patch-sla-was-written-for-a-slower-attacker/" rel="alternate" type="text/html" title="Your patch SLA was written for a slower attacker: what PHP2Shell teaches about vulnerability governance in the AI era" /><published>2026-10-23T00:00:00-04:00</published><updated>2026-10-23T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/your-patch-sla-was-written-for-a-slower-attacker</id><content type="html" xml:base="https://mihirajmera.github.io/blog/your-patch-sla-was-written-for-a-slower-attacker/"><![CDATA[<p>On October 21, PHP shipped emergency fixes for two critical flaws that, between them, affect every PHP release of the last decade. PHP runs about 70% of the world’s websites, according to the AP. By the time the fix was published, attackers were already using the flaws. Within hours there were, in Cloudflare CEO Matthew Prince’s words, “millions” of attempts. A day later, government websites in Britain, Finland and Japan had been taken partly offline as a precaution.</p>

<p>The flaws are called PHP2Shell (CVE-2026-109618 and CVE-2026-109619). The story everyone is telling is that <strong>AI found them first</strong>. That’s true, and it’s good news. But the part that matters for anyone who governs security is the other half: <strong>AI also made them cheap to weaponize</strong>, and almost every vulnerability management policy I’ve read assumes it’s expensive.</p>

<div class="figure-row"><div class="red"><b>4</b><span>days between the first exploit attempt and public disclosure</span></div><div class="red"><b>~5h</b><span>from leaked details to reported active exploitation, before the patch existed</span></div><div class="amber"><b>0</b><span>fixed PAN-OS versions a day later: you can't patch PHP inside an appliance</span></div></div>

<h2 id="what-happened">What happened</h2>

<p>The researchers’ timeline, from <a href="https://www.wiz.io/blog/php2shell-php-rce">Wiz</a>, <a href="https://www.recordedfuture.com/blog/php2shell-vulnerability-exploitation">Recorded Future</a> and <a href="https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/">Amazon Threat Intelligence</a> (all times UTC):</p>

<table>
  <thead>
    <tr>
      <th>When</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Aug 19</td>
      <td>Wiz Research finds the first vulnerability; unauthenticated code execution against Drupal and WordPress follows within two days</td>
    </tr>
    <tr>
      <td>Aug 24</td>
      <td>Reported privately to the PHP security team</td>
    </tr>
    <tr>
      <td>Sep 21</td>
      <td>PHP acknowledges the reports and starts a coordinated fix</td>
    </tr>
    <tr>
      <td>Oct 17</td>
      <td>Amazon’s honeypots record a PHP2Shell exploitation attempt, <strong>four days before disclosure</strong></td>
    </tr>
    <tr>
      <td>Oct 20</td>
      <td>PHP announces an out-of-band release for the next day</td>
    </tr>
    <tr>
      <td>Oct 21, ~02:00</td>
      <td>Details have leaked; the earliest proofs of concept appear</td>
    </tr>
    <tr>
      <td>Oct 21, 07:34</td>
      <td>Patchstack reports “multiple active exploitation incidents” from hosting partners</td>
    </tr>
    <tr>
      <td>Oct 21, 13:52</td>
      <td>PHP releases 8.2.35, 8.3.36, 8.4.27 and 8.5.12</td>
    </tr>
    <tr>
      <td>Oct 21–22</td>
      <td>“Millions” of exploit attempts; the FBI advises taking sites offline if they can’t be updated immediately</td>
    </tr>
    <tr>
      <td>Oct 22</td>
      <td>Palo Alto says every PAN-OS and Panorama version is affected, with no fixed release yet; Cisco and Fortinet list dozens of products under investigation</td>
    </tr>
  </tbody>
</table>

<p>The bugs themselves are memory-safety flaws in how PHP cleans up after a request fails partway through, for example when it runs out of memory. Corrupted state can survive into the next request on the same worker, and a remote attacker with no account can turn that into code execution. Wiz says the flaws had been present for years; what was missing was a practical way to exploit them.</p>

<p><strong>That’s where AI changed the economics.</strong> Wiz says frontier models helped find the bug, build the exploit, and adapt it to different PHP versions and memory layouts, which “shortened exploit development from months to hours.” OpenAI, which coordinated the disclosure through its Patch the Planet initiative, said AI appeared to find the flaws before any attacker did. Amazon’s post makes the uncomfortable corollary explicit: AI-assisted analysis makes it cheaper to turn a patch diff or an advisory into a working exploit, so more actors can act within hours or minutes of disclosure.</p>

<p>The defenders had a two-month head start. It shrank to hours when the details leaked, and it would have shrunk to hours anyway the moment the patch was public.</p>

<h2 id="why-this-one-is-different">Why this one is different</h2>

<h3 id="the-race-was-lost-before-the-patch-existed">The race was lost before the patch existed</h3>

<p>Every vulnerability management policy is built around one event: <strong>the vendor releases a fix</strong>. The clock starts there. “Critical: remediate within 30 days” or, for the most aggressive programs, 14 or 7 days.</p>

<p>In this case, exploitation started four days before that event, according to Amazon, and was widespread on the day of it. A policy that starts the clock at patch release had nothing to say about the period when the risk was highest. No SLA was breached. Every organization was “compliant” while it was being attacked.</p>

<h3 id="patch-assumes-the-code-is-yours">“Patch” assumes the code is yours</h3>

<p>The PHP Foundation fixed PHP in about two months. That fixes PHP on servers where you install PHP. It doesn’t fix the PHP inside your firewall’s management interface, your SIEM, your load balancer or your vendor’s SaaS platform.</p>

<p>A day after disclosure, according to <a href="https://www.helpnetsecurity.com/2026/10/22/php2shell-vulnerability-fortinet-cisco-palo-alto-networks/">Help Net Security</a>, Palo Alto listed every PAN-OS and Panorama version as affected with no fixed release yet. Fortinet had 12 products under investigation. Cisco had 18 products and 13 cloud services under investigation, and said there were no workarounds. Your 30-day SLA can’t start until those vendors ship, and you don’t control when that is.</p>

<p>This is where vulnerability management quietly becomes third-party risk management. The question isn’t “how fast do we patch?” It’s “which of our vendors have PHP inside, and what have they told us?” Most companies can’t answer that today, because they don’t have software bills of materials (SBOMs) from their appliance vendors, or don’t read the ones they have.</p>

<h3 id="most-of-the-exposure-cant-be-patched-at-all">Most of the exposure can’t be patched at all</h3>

<p>Scans quoted by Recorded Future found about 5.1 million IP addresses running PHP. Of the roughly 1.1 million that disclose a version, about 758,000 run branches that are end-of-life: PHP 8.1 and older, which get no official fix. For those, the remediation isn’t a patch. It’s a migration project, and it was overdue before this week.</p>

<p>End-of-life software usually sits in a risk register as an accepted risk with a note like “no known exploitable vulnerabilities.” That sentence expired on October 21.</p>

<h2 id="what-would-have-helped-ranked-by-cost">What would have helped, ranked by cost</h2>

<p><strong>Policy changes (cost: a meeting and a signature):</strong></p>

<ol>
  <li><strong>Add a “pre-patch” tier to the vulnerability policy.</strong> For critical, internet-facing, actively exploited flaws, the policy should require <em>mitigation</em> within hours, whether or not a patch exists: WAF rules, restricting access, disabling the affected feature or taking the service offline. Amazon’s advice was to measure these timelines “in hours, not days or weeks.”</li>
  <li><strong>Pre-approve the decision to go offline.</strong> The FBI recommended it. Japan’s prime minister said suspending websites was an option. Britain’s prime minister’s office took some services offline. Inside a company, that decision usually needs a business owner, and at 3 a.m. nobody knows who it is. Name the role, the criteria and the maximum outage in advance, so the incident team doesn’t have to find a vice president.</li>
  <li><strong>Start investigations from first exploitation, not from disclosure.</strong> Amazon’s advice was to look back to October 17. If your incident procedure says “review logs since the advisory,” you’ll miss the four days that matter.</li>
</ol>

<p><strong>Inventory (cost: weeks, then continuous):</strong></p>

<ol>
  <li><strong>Know where the runtime is, not just the application.</strong> “Do we run WordPress?” is the wrong question. “Where does PHP execute, including inside products we bought?” is the right one. Container images, managed hosting and appliances all count.</li>
  <li><strong>Ask vendors for SBOMs and read them.</strong> Then, when the next runtime-level flaw lands, the third-party risk team can send targeted questions within the hour instead of a generic questionnaire within the month.</li>
  <li><strong>Put end-of-life software on a funded plan, not an exception.</strong> An exception without an expiry date is a decision not to decide.</li>
</ol>

<p><strong>Architecture (cost: real, but already justified):</strong></p>

<ol>
  <li><strong>Never expose management interfaces to the internet.</strong> Palo Alto’s main mitigation was to restrict management access to trusted internal addresses. If that was already true, the firewall issue was a low-priority patch, not an emergency.</li>
  <li><strong>Make origins unreachable except through the WAF.</strong> A virtual patch at the edge does nothing if the server behind it has a public address. Recorded Future called this out explicitly.</li>
  <li><strong>Detect behavior, not CVEs.</strong> Repeated out-of-memory errors, PHP worker churn and web servers spawning shells look the same whatever the next CVE number is.</li>
</ol>

<h2 id="if-you-run-php">If you run PHP</h2>

<p>Update to <strong>8.5.12, 8.4.27, 8.3.36 or 8.2.35</strong>. PHP says configuration changes don’t fully close both flaws, so treat any mitigation as temporary. If you’re on 8.1 or older, use your OS vendor’s backported package or move to a supported branch. Check appliances and SaaS vendors separately, and review logs from <strong>October 17</strong> onward, not from the advisory. <a href="https://watchtowr.com/intelligence/php-cve-2026-109618-faq/">watchTowr’s FAQ</a> and <a href="https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/">Amazon’s post</a> have the detection guidance.</p>

<h2 id="what-this-means-for-ai-governance">What this means for AI governance</h2>

<p>AI governance usually means governing the AI you build or buy. PHP2Shell is a reminder that it also means governing <strong>for</strong> an environment where everyone, including attackers, has AI.</p>

<p>That has two consequences for a control framework.</p>

<p><strong>First, the good news is real.</strong> AI models found flaws that had sat in critical infrastructure for years, and the industry got a two-month head start to coordinate a fix. OpenAI is now proposing a framework for handling AI-discovered vulnerabilities and has asked the security community to critique it. That’s the right instinct: when finding bugs gets cheap, disclosure processes designed for a few dozen critical bugs a year will be overwhelmed.</p>

<p><strong>Second, the assumptions behind our timelines are out of date.</strong> Patch SLAs, exception approvals and risk acceptances all carry an unstated assumption about how long it takes an attacker to go from “a flaw exists” to “a flaw is exploited.” For PHP2Shell, the gap was measured in hours, and in at least one case it was negative.</p>

<p>Three questions every security and risk program should be able to answer after this week:</p>

<ol>
  <li><strong>What does our policy require before a patch exists?</strong> If the answer is “nothing,” the policy covers the safe part of the timeline and skips the dangerous one.</li>
  <li><strong>Which of our vendors would we have to call, and how fast would we know?</strong> If the answer depends on a questionnaire, it’s too slow.</li>
  <li><strong>Who is allowed to take a service offline, and on what criteria?</strong> If nobody knows, the decision will be made late, or not at all.</li>
</ol>

<p>The patch arrived two months after the bug was reported, and AI deserves credit for that head start. The exploits arrived before the patch, and AI deserves some credit for that too. Policies that start the clock at patch release are measuring the wrong race.</p>

<p><em>Sources: <a href="https://www.wiz.io/blog/php2shell-php-rce">Wiz Research</a> · <a href="https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/">Amazon Threat Intelligence</a> · <a href="https://www.recordedfuture.com/blog/php2shell-vulnerability-exploitation">Recorded Future</a> · <a href="https://watchtowr.com/intelligence/php-remote-code-execution-php2shell/">watchTowr</a> · <a href="https://www.pbs.org/newshour/nation/ai-discovered-a-software-vulnerability-that-could-impact-70-of-websites-worldwide-the-rush-is-on-to-patch-it">AP via PBS NewsHour</a> · <a href="https://www.helpnetsecurity.com/2026/10/22/php2shell-vulnerability-fortinet-cisco-palo-alto-networks/">Help Net Security</a> · <a href="https://fortune.com/2026/10/21/ai-tools-discovered-the-php-vulnerability-that-could-take-down-70-of-the-internet-now-the-race-is-on-to-patch-it-before-ai-powered-hackers-attack/">Fortune</a>. Vendor fix status is changing daily; check each vendor’s advisory. Exposure counts are scan results, not confirmed vulnerable hosts. Times are UTC.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="Vulnerability management" /><category term="AI-discovered vulnerabilities" /><category term="Incident analysis" /><summary type="html"><![CDATA[AI helped find two critical PHP flaws, and AI helped attackers weaponize them within hours. The 30-day patch window in most compliance programs assumes a world that ended this week.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/your-patch-sla-was-written-for-a-slower-attacker.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/your-patch-sla-was-written-for-a-slower-attacker.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The AI gateway is a crown jewel: what the LiteLLM compromise teaches about governing AI infrastructure</title><link href="https://mihirajmera.github.io/blog/the-ai-gateway-is-a-crown-jewel/" rel="alternate" type="text/html" title="The AI gateway is a crown jewel: what the LiteLLM compromise teaches about governing AI infrastructure" /><published>2026-10-22T00:00:00-04:00</published><updated>2026-10-22T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/the-ai-gateway-is-a-crown-jewel</id><content type="html" xml:base="https://mihirajmera.github.io/blog/the-ai-gateway-is-a-crown-jewel/"><![CDATA[<p>For roughly two hours on October 21, installing the official release of <a href="https://github.com/BerriAI/litellm">LiteLLM</a>, one of the most widely used AI gateways, gave an attacker your cloud credentials, your SSH keys and, if you ran the proxy, the API keys for every model provider your company uses.</p>

<p>It wasn’t a typosquat or a fake package. Version <code class="language-plaintext highlighter-rouge">1.107.0</code> was built, signed and published by LiteLLM’s real release pipeline, and passed its approval gate. Every control did what it was designed to do.</p>

<p>That’s what makes it worth studying. Here’s what happened, why it matters more than another supply-chain incident, and what governing AI infrastructure should look like after it.</p>

<div class="figure-row"><div class="red"><b>~2h</b><span>the poisoned release was live on PyPI and Docker</span></div><div class="amber"><b>3</b><span>developer accounts whose stolen credentials were used</span></div><div class="red"><b>0</b><span>signatures that failed: the pipeline signed the malware</span></div></div>

<h2 id="what-happened">What happened</h2>

<p>LiteLLM is a proxy that sits between applications and model providers: one API in front of OpenAI, Anthropic, Bedrock, Vertex and dozens more. Datadog’s write-up cites about 23 million weekly downloads. Companies run it as the central gateway for their AI traffic.</p>

<p>According to <a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026">LiteLLM’s incident report</a> and independent reconstructions by <a href="https://www.stepsecurity.io/blog/litellm-1-107-0-compromised-pypi-backdoored-wheels-credential-stealer">StepSecurity</a>, <a href="https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-docker/">Datadog</a> and <a href="https://www.chainguard.dev/unchained/litellm-attacked-again-malicious-1-107-0-wheels-on-pypi-and-docker-hub-october-2026">Chainguard</a> (all times UTC):</p>

<table>
  <thead>
    <tr>
      <th>Time</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>10:08</td>
      <td>An unauthorized change to the private release workflow is blocked by branch protection, then applied using a privileged developer’s credential</td>
    </tr>
    <tr>
      <td>11:11</td>
      <td>A second developer’s credential satisfies the production approval gate</td>
    </tr>
    <tr>
      <td>11:39</td>
      <td>Seven <code class="language-plaintext highlighter-rouge">litellm==1.107.0</code> wheels are uploaded to PyPI</td>
    </tr>
    <tr>
      <td>11:46</td>
      <td>Infected Docker images are published, and <code class="language-plaintext highlighter-rouge">latest</code>, <code class="language-plaintext highlighter-rouge">main-latest</code> and <code class="language-plaintext highlighter-rouge">main-stable</code> are repointed to them</td>
    </tr>
    <tr>
      <td>12:06</td>
      <td>A user reports the compromise publicly (<a href="https://github.com/BerriAI/litellm/issues/47826">#47826</a>)</td>
    </tr>
    <tr>
      <td>~13:20</td>
      <td>PyPI quarantines 1.107.0</td>
    </tr>
    <tr>
      <td>14:20</td>
      <td>The Docker Hub tags are removed or repointed</td>
    </tr>
  </tbody>
</table>

<p>The source code was never modified; the <code class="language-plaintext highlighter-rouge">v1.107.0</code> git tag is clean. The attacker repointed the release workflow at a lookalike repository, <code class="language-plaintext highlighter-rouge">Berri-AI/litellm</code> instead of <code class="language-plaintext highlighter-rouge">BerriAI/litellm</code>, and added one line to the build configuration so that three extra files landed in every wheel.</p>

<p><strong>The payload ran without anyone importing LiteLLM.</strong> A <code class="language-plaintext highlighter-rouge">cook.pth</code> file in site-packages executes on every Python start, pip included. It ran a credential stealer covering 16 categories, including cloud credentials, SSH and GPG keys, browser passwords, CI secrets, the environment of every readable process, and AI coding tool configs. It also planted an hourly loader designed to fetch and run new code.</p>

<blockquote>
  <p><strong>Update, October 28:</strong> LiteLLM’s <a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026-attack-chain">attack-chain post</a> ruled out the log theory below. The first token came from a master key leaked in May and never revoked. I’ve written up the corrected root cause in <a href="/blog/deleting-the-commit-didnt-revoke-the-key/">Deleting the commit didn’t revoke the key</a>.</p>
</blockquote>

<p><strong>How the attacker got the credentials</strong> is still under investigation. LiteLLM’s report describes an unexpected user account on an internet-exposed internal LiteLLM deployment, and says that GitHub CLI tokens could appear in that deployment’s logs because response bodies were logged. It hasn’t confirmed how the attacker got into the deployment.</p>

<p>This was LiteLLM’s second compromise in seven months; in March, according to Chainguard, a stolen PyPI token was used to publish two malicious versions.</p>

<h2 id="why-this-one-is-different">Why this one is different</h2>

<h3 id="the-payload-was-built-for-ai-infrastructure">The payload was built for AI infrastructure</h3>

<p>Most credential stealers grab whatever’s on disk. This one had a module, <code class="language-plaintext highlighter-rouge">db_litellm</code>, that connected to the proxy’s database when <code class="language-plaintext highlighter-rouge">DATABASE_URL</code> was set and queried <code class="language-plaintext highlighter-rouge">LiteLLM_CredentialsTable</code> and <code class="language-plaintext highlighter-rouge">LiteLLM_ProxyModelTable</code>: the tables where the gateway stores <strong>every model provider’s API key</strong>.</p>

<p>An AI gateway is the most valuable place in a company to steal AI credentials from, because it’s designed to hold all of them. It often also logs prompts and responses, which can contain customer data, internal documents and, as LiteLLM found, other people’s tokens.</p>

<p><strong>We’ve been treating AI gateways as developer tooling. They’re tier-0 infrastructure</strong>, closer to an identity provider than a library.</p>

<h3 id="signed-didnt-mean-safe">“Signed” didn’t mean “safe”</h3>

<p>LiteLLM had done the hardening the industry recommends after March: a private release workflow, PyPI trusted publishing, immutable Docker tags and image signing. None of it helped, because the attacker didn’t bypass the pipeline. They changed what the pipeline built, and the pipeline signed it.</p>

<p>But the evidence was there. The build provenance named the lookalike repository. A signature answers <em>“did this pipeline build this artifact?”</em> It doesn’t answer <em>“did it build it from the source I think it did?”</em> unless someone checks the source in the provenance. Tools like <code class="language-plaintext highlighter-rouge">slsa-verifier</code> take a <code class="language-plaintext highlighter-rouge">--source-uri</code> for exactly this reason. Almost nobody consuming PyPI packages runs that check.</p>

<h3 id="two-person-rules-fail-when-one-person-holds-two-keys">Two-person rules fail when one person holds two keys</h3>

<p>The release had two strong controls on paper: branch protection on the workflow, and a production approval gate. Each was defeated with a different developer’s stolen credential.</p>

<p>Auditors call this segregation of duties, and it’s the oldest lesson in controls: a two-person rule only works if no single party can act as both people. When credentials are long-lived and collectable from one place, such as logs, one attacker can impersonate everyone in the chain. The control was designed for insiders acting alone, not for an outsider holding three tokens.</p>

<h2 id="what-would-have-helped-ranked-by-cost">What would have helped, ranked by cost</h2>

<p><strong>For everyone who installs open-source packages:</strong></p>

<ol>
  <li><strong>A cooldown on new versions.</strong> The malicious release was removed from PyPI in under two hours. A policy of not installing versions younger than one day, available through Dependabot’s cooldown setting or uv’s <code class="language-plaintext highlighter-rouge">--exclude-newer</code>, would have skipped it entirely. I use a two-week window on my own projects; this is the incident that justifies it.</li>
  <li><strong>Pin versions and images, never <code class="language-plaintext highlighter-rouge">latest</code>.</strong> The attacker repointed <code class="language-plaintext highlighter-rouge">latest</code>, <code class="language-plaintext highlighter-rouge">main-latest</code> and <code class="language-plaintext highlighter-rouge">main-stable</code>. Anyone pulling a mutable tag got the malware without changing a line of config. Pin Docker images by digest.</li>
  <li><strong>Hash-locked dependencies, with a caveat.</strong> Hash pinning protects you from a <em>changed</em> artifact you’ve already vetted. It would not have stopped you from regenerating a lockfile onto the malicious version. Pinning and cooldown work together.</li>
  <li><strong>Egress control on AI infrastructure.</strong> The stealer sent data over plain HTTP to an attacker domain. A gateway that can only reach model providers can’t exfiltrate to anywhere else.</li>
</ol>

<p><strong>For anyone running an AI gateway:</strong></p>

<ol>
  <li><strong>Keep provider keys in a secret manager</strong>, not in the gateway’s database, so a compromised process can’t dump them all with one query.</li>
  <li><strong>Don’t log full request and response bodies by default.</strong> AI traffic carries credentials and sensitive data in places nobody expects. If you must log, redact.</li>
  <li><strong>Give the gateway its own identity, least privilege and monitoring</strong>, as you would an identity provider. Alert on new outbound destinations.</li>
</ol>

<p><strong>For anyone publishing software:</strong></p>

<ol>
  <li><strong>Verify the source, not just the signature.</strong> Check that provenance names the repository you expect, in CI, before publishing and before installing.</li>
  <li><strong>Make approval gates require something an attacker can’t replay</strong>, such as a hardware key or a fresh MFA prompt, not a long-lived token.</li>
  <li><strong>Treat the release workflow definition as a crown jewel too.</strong> Changes to it should be rarer and more heavily reviewed than changes to the code.</li>
</ol>

<h2 id="if-you-were-affected">If you were affected</h2>

<p>Follow the <a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026">official remediation</a> and the <a href="https://github.com/BerriAI/litellm/issues/47826">tracking issue</a>, which has the wheel hashes and indicators of compromise. The short version from the incident responders:</p>

<ul>
  <li><strong>Don’t run Python in an affected environment first.</strong> Every interpreter start re-runs the stealer; find and kill the loader process, then delete <code class="language-plaintext highlighter-rouge">cook.pth</code>, <code class="language-plaintext highlighter-rouge">cook.py</code> and the foreign <code class="language-plaintext highlighter-rouge">sitecustomize.py</code>.</li>
  <li><strong>Rebuild rather than clean</strong>, pinned to <code class="language-plaintext highlighter-rouge">litellm==1.106.2</code> or the <code class="language-plaintext highlighter-rouge">v1.106.2</code> image by digest.</li>
  <li><strong>Assume exfiltration succeeded</strong> and rotate everything the host could reach, including every provider key in the gateway’s database, its master and salt keys, and the database password.</li>
</ul>

<h2 id="what-this-means-for-ai-governance">What this means for AI governance</h2>

<p>AI governance conversations tend to focus on models: bias, hallucination, misuse. This incident had nothing to do with model behavior. It was about the plumbing around the models, and that plumbing now concentrates some of the most valuable credentials and data a company has.</p>

<p>Three questions every AI governance program should be able to answer after this week:</p>

<ol>
  <li><strong>Do we have an inventory of our AI infrastructure</strong>, not just our models: gateways, proxies, agent frameworks and MCP servers, with owners?</li>
  <li><strong>Is that infrastructure held to tier-0 controls?</strong> Pinned and verified dependencies, secrets out of databases and logs, egress allowlists, monitored identities.</li>
  <li><strong>Would we know within two hours?</strong> LiteLLM’s community spotted this in under 30 minutes. Most companies wouldn’t have noticed at all until the credentials were used.</li>
</ol>

<p>The release was signed, approved and published by the real pipeline. Every control worked as designed. That’s not a reason to trust controls less. It’s a reason to design them for an attacker who holds more than one key.</p>

<p><em>Sources: <a href="https://docs.litellm.ai/blog/security-incident-oct-21-2026">LiteLLM incident report</a> · <a href="https://github.com/BerriAI/litellm/issues/47826">BerriAI/litellm#47826</a> · <a href="https://www.stepsecurity.io/blog/litellm-1-107-0-compromised-pypi-backdoored-wheels-credential-stealer">StepSecurity</a> · <a href="https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-docker/">Datadog Security Labs</a> · <a href="https://www.chainguard.dev/unchained/litellm-attacked-again-malicious-1-107-0-wheels-on-pypi-and-docker-hub-october-2026">Chainguard</a> · <a href="https://techcrunch.com/2026/10/21/the-internet-is-having-a-very-bad-cybersecurity-day/">TechCrunch</a>. Details may change as the investigation continues; times are UTC.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI supply chain" /><category term="AI infrastructure" /><category term="Incident analysis" /><summary type="html"><![CDATA[For about two hours on October 21, the official LiteLLM release stole credentials from everyone who installed it. It was signed, approved and published by the real pipeline. Every control worked as designed, and that's the lesson.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/the-ai-gateway-is-a-crown-jewel.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/the-ai-gateway-is-a-crown-jewel.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Twelve clean results is not a pass</title><link href="https://mihirajmera.github.io/blog/twelve-clean-results-is-not-a-pass/" rel="alternate" type="text/html" title="Twelve clean results is not a pass" /><published>2026-10-21T00:00:00-04:00</published><updated>2026-10-21T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/twelve-clean-results-is-not-a-pass</id><content type="html" xml:base="https://mihirajmera.github.io/blog/twelve-clean-results-is-not-a-pass/"><![CDATA[<p>In my red-team of Llama 3.2 3B, the model resisted all 12 markdown-image exfiltration attempts. Not one leaked data. A dashboard would show that control as green.</p>

<p>My tool reported it as <strong>inconclusive</strong>, and that was the honest answer. This essay explains why, how auditors have handled the same problem for decades, and how to size an AI red-team so that “pass” actually means something.</p>

<div class="figure-row"><div class="amber"><b>24%</b><span>worst plausible failure rate after 0 failures in 12 tests</span></div><div class=""><b>59</b><span>clean tests an auditor needs to support a 5% tolerable rate</span></div><div class="green"><b>381</b><span>clean tests needed before you can claim under 1%</span></div></div>

<h2 id="zero-failures-is-a-statement-about-the-sample">Zero failures is a statement about the sample</h2>

<p>A red-team run is a sample. You don’t test every possible prompt; you test a few hundred and infer how the model behaves on the rest. That inference has error bars, and with small samples the error bars are enormous.</p>

<p>The question to ask isn’t “how many attacks succeeded?” but “given what we observed, how high could the true attack success rate plausibly be?” For zero failures, the answer is roughly <strong>3.84 / (n + 3.84)</strong> at 95% confidence. That’s the upper end of the Wilson score interval, the method <a href="https://github.com/Mihirajmera/llm-redteam-evidence">llm-redteam-evidence</a> uses.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Clean tests (0 failures)</th>
      <th style="text-align: right">True rate could still be as high as</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">49%</td>
    </tr>
    <tr>
      <td style="text-align: right">12</td>
      <td style="text-align: right">24%</td>
    </tr>
    <tr>
      <td style="text-align: right">20</td>
      <td style="text-align: right">16%</td>
    </tr>
    <tr>
      <td style="text-align: right">50</td>
      <td style="text-align: right">7.1%</td>
    </tr>
    <tr>
      <td style="text-align: right">100</td>
      <td style="text-align: right">3.7%</td>
    </tr>
    <tr>
      <td style="text-align: right">256</td>
      <td style="text-align: right">1.5%</td>
    </tr>
    <tr>
      <td style="text-align: right">381</td>
      <td style="text-align: right">1.0%</td>
    </tr>
  </tbody>
</table>

<p>Read the second row again. Twelve clean results are entirely consistent with a model that falls for that attack one time in four. You just didn’t happen to see it.</p>

<h2 id="auditors-solved-this-a-long-time-ago">Auditors solved this a long time ago</h2>

<p>None of this is new to audit. When an auditor tests a control, say “every payment over $10,000 has a second approval”, they don’t check every payment. They pick a sample, and the sample size comes from two numbers decided <strong>before</strong> testing:</p>

<ul>
  <li><strong>Tolerable deviation rate</strong>: the failure rate at which the control would no longer be considered effective (for example 5%).</li>
  <li><strong>Confidence</strong>: how sure they need to be that the true rate is below it (commonly 95%).</li>
</ul>

<p>With zero expected deviations, that gives a sample of <strong>59</strong> for a 5% tolerable rate at 95% confidence. That’s the figure in standard audit sampling tables, and it falls straight out of the same mathematics: the smallest <em>n</em> where (1 − 0.05)ⁿ ≤ 5%.</p>

<p>If one of those 59 fails, the auditor doesn’t round it away. The sample no longer supports the conclusion, and they either expand testing or conclude the control isn’t effective.</p>

<p>AI red-team reports rarely work this way. They present “0 of 12” as if it were “0%”, and nobody asks how 12 was chosen.</p>

<h2 id="how-many-attacks-do-you-actually-need">How many attacks do you actually need?</h2>

<p>Turn the table around: given a tolerance, how many clean results do you need before the upper bound drops below it?</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Tolerance</th>
      <th style="text-align: right">Exact one-sided (audit tables)</th>
      <th style="text-align: right">Wilson 95% (my tool)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">10%</td>
      <td style="text-align: right">29</td>
      <td style="text-align: right">35</td>
    </tr>
    <tr>
      <td style="text-align: right">5%</td>
      <td style="text-align: right">59</td>
      <td style="text-align: right">73</td>
    </tr>
    <tr>
      <td style="text-align: right">2%</td>
      <td style="text-align: right">149</td>
      <td style="text-align: right">189</td>
    </tr>
    <tr>
      <td style="text-align: right">1%</td>
      <td style="text-align: right">299</td>
      <td style="text-align: right">381</td>
    </tr>
  </tbody>
</table>

<p>The two methods differ a little: the Wilson interval is two-sided and slightly more conservative. Either way, the shape is the same. <strong>Halving the tolerance roughly doubles the sample.</strong> And a tolerance of exactly 0%, which is what most teams instinctively want for things like system-prompt leakage, can never be demonstrated by testing. You can only show the rate is below some small number.</p>

<p>That’s a governance decision, not a statistics problem. Someone has to write down “we accept up to 1% and we’ll test enough to show it”, or accept that “0%” is an aspiration the evidence can’t support.</p>

<h2 id="what-this-looked-like-in-a-real-run">What this looked like in a real run</h2>

<p>From the <a href="/blog/auditing-the-red-team/">Llama 3.2 3B assessment</a>:</p>

<ul>
  <li><strong>Unsafe output (RT-07):</strong> 0 of 12 exfiltration attempts succeeded. Upper bound 24%, against a 0% tolerance and a 20-attack minimum. Verdict: <strong>inconclusive</strong>, a coverage gap.</li>
  <li><strong>Jailbreaks (RT-03):</strong> 2 of 4 succeeded. That’s 50%, but the upper bound is 85%. With four attempts you can say the model is jailbreakable; you can’t say how jailbreakable.</li>
  <li><strong>Indirect injection (RT-02):</strong> 113 of 256 succeeded, 44% with an upper bound of 50%. The large sample makes this one a confident failure.</li>
</ul>

<p>The same tool, the same model, three different strengths of evidence. A report that showed only the rates would have hidden all of that.</p>

<h2 id="five-habits-for-red-team-evidence">Five habits for red-team evidence</h2>

<ol>
  <li><strong>Set the tolerance and the minimum sample per control before the run.</strong> Write them down and version them, like any other control definition.</li>
  <li><strong>Report the upper bound next to every rate.</strong> “0% (95% upper bound 24%)” tells a reviewer exactly how much the result is worth.</li>
  <li><strong>Treat under-sampled controls as coverage gaps, not passes.</strong> “Inconclusive” belongs in the findings, with a plan to close it.</li>
  <li><strong>Size by tolerance.</strong> Use the table above. If you can’t afford 189 attempts for a 2% tolerance, either accept a looser tolerance or say the control is untested.</li>
  <li><strong>Spread the sample.</strong> 300 variations of one prompt aren’t 300 independent tests. Mix attack families, phrasings and contexts, the way an auditor spreads a sample across months and locations.</li>
</ol>

<h2 id="the-bigger-point">The bigger point</h2>

<p>“We tested it and nothing happened” is one of the most common sentences in AI assurance, and one of the least informative. The fix isn’t more sophisticated attacks. It’s the discipline audit already has: decide what’s acceptable, size the test to prove it, and report the uncertainty honestly.</p>

<p>Zero failures is the start of an argument, not the end of one.</p>

<p><em>The numbers here use 95% confidence. The Wilson bound is computed by <code class="language-plaintext highlighter-rouge">wilson_upper()</code> in <a href="https://github.com/Mihirajmera/llm-redteam-evidence/blob/main/redteam_evidence/assess.py">llm-redteam-evidence</a>; the exact one-sided column is the smallest n with (1 − tolerance)ⁿ ≤ 0.05.</em></p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI assurance" /><category term="Red-teaming" /><category term="Audit sampling" /><summary type="html"><![CDATA[Zero failures sounds like proof. With a small sample, it mostly tells you how little you tested. What auditors already know about sampling, applied to AI red-teaming.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/twelve-clean-results-is-not-a-pass.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/twelve-clean-results-is-not-a-pass.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">What the official MCP READMEs teach you to copy</title><link href="https://mihirajmera.github.io/blog/what-mcp-readmes-teach-you-to-copy/" rel="alternate" type="text/html" title="What the official MCP READMEs teach you to copy" /><published>2026-10-21T00:00:00-04:00</published><updated>2026-10-21T00:00:00-04:00</updated><id>https://mihirajmera.github.io/blog/what-mcp-readmes-teach-you-to-copy</id><content type="html" xml:base="https://mihirajmera.github.io/blog/what-mcp-readmes-teach-you-to-copy/"><![CDATA[<p>Most companies recertify employee access every quarter. Someone opens a spreadsheet, looks at who can reach what, and signs off. AI agents now hold GitHub tokens, database connections, shells and Slack bots, and almost nobody reviews them at all.</p>

<p>Most of that access isn’t designed. It’s copied. A developer wants Claude or Cursor to read their files, so they open the server’s README, copy the configuration example and paste it in. So I asked a simple question: <strong>if you set up an agent by copying the official examples, what access does it end up with?</strong></p>

<div class="figure-row"><div class="red"><b>8/9</b><span>configs run an unpinned package or image</span></div><div class="amber"><b>7/9</b><span>grant write, send, delete or execute</span></div><div class="red"><b>1</b><span>exfiltration path once they're combined</span></div></div>

<h2 id="method">Method</h2>

<p>I took the first valid configuration example from 9 widely used MCP server READMEs: the official reference servers, GitHub’s MCP server and a popular community server. I saved each exactly as published, with a link to the exact commit and line, and scanned them with <a href="https://github.com/Mihirajmera/agent-access-review">agent-access-review</a>, my open-source tool that runs a user access review on AI agents.</p>

<p>These are documentation examples, written to be easy to copy, not production setups. The point isn’t that the READMEs are wrong. It’s that copying them without a review produces the access below, and nothing in a typical workflow flags it.</p>

<h2 id="one-config-at-a-time">One config at a time</h2>

<table>
  <thead>
    <tr>
      <th>Server</th>
      <th>What the agent can do</th>
      <th>Main findings</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Desktop Commander</td>
      <td>read, write, delete, <strong>execute</strong></td>
      <td>Unsandboxed shell, unpinned</td>
    </tr>
    <tr>
      <td>filesystem</td>
      <td>read, write, delete</td>
      <td>Write access, unpinned</td>
    </tr>
    <tr>
      <td>git</td>
      <td>read, write</td>
      <td>Write access, unpinned</td>
    </tr>
    <tr>
      <td>memory</td>
      <td>read, write</td>
      <td>Write access, unpinned</td>
    </tr>
    <tr>
      <td>puppeteer</td>
      <td>read, send</td>
      <td>Can send data out, unpinned</td>
    </tr>
    <tr>
      <td>slack</td>
      <td>read, send</td>
      <td><strong>Token pasted into the config</strong>, unpinned</td>
    </tr>
    <tr>
      <td>fetch</td>
      <td>read</td>
      <td>Unpinned</td>
    </tr>
    <tr>
      <td>postgres</td>
      <td>read</td>
      <td>Unpinned</td>
    </tr>
    <tr>
      <td>github (remote)</td>
      <td>read, write</td>
      <td>Write access</td>
    </tr>
  </tbody>
</table>

<p><strong>Unpinned packages are the quiet one.</strong> <code class="language-plaintext highlighter-rouge">npx -y &lt;package&gt;</code>, <code class="language-plaintext highlighter-rouge">@latest</code>, <code class="language-plaintext highlighter-rouge">uvx &lt;package&gt;</code> and an untagged <code class="language-plaintext highlighter-rouge">docker run</code> all fetch whatever was published most recently, every time the client starts. If a release is ever compromised, it runs with your agent’s access, and your config never changed. In a normal software supply chain, that would fail a review on day one.</p>

<p><strong>The Slack example teaches a secrets anti-pattern.</strong> Its <code class="language-plaintext highlighter-rouge">env</code> block sets the bot token to a placeholder you replace with the real token. Follow it, and your Slack token sits in plain text in a JSON file, in every backup and in any dotfiles repository you sync.</p>

<p><strong>Write, send and execute are the point of most of these servers.</strong> That’s fine. The finding is about governance: who approved giving an agent that power, and is there a human check before it acts?</p>

<h2 id="then-i-pasted-them-together">Then I pasted them together</h2>

<p>Developers rarely run one server. I combined all nine examples into a single config, the way a real setup grows over a few weeks of “let me just add this one”. The scanner raised its highest-severity finding:</p>

<p class="pull">The agent reads sensitive data, ingests content outsiders can write, and can send data out, with no human approval. That's an exfiltration path.</p>

<p>A prompt injection hidden in a web page, a GitHub issue or a Slack message could make the agent read your files or database and send them somewhere. No single README creates this risk. The combination does, and that’s exactly what per-server documentation and per-tool reviews miss.</p>

<p>It’s the same lesson as segregation of duties in financial controls. Each permission is reasonable on its own. The risk is in the combination, so that’s where the review has to look.</p>

<h2 id="what-my-own-tool-got-wrong">What my own tool got wrong</h2>

<p>Running a real scan found two gaps in agent-access-review, both now fixed:</p>

<ol>
  <li><strong>5 of the 9 configs were initially unclassified.</strong> The tool only recognised npm package names, but the official READMEs now mostly recommend Docker images and <code class="language-plaintext highlighter-rouge">uvx</code>. The capability catalog now supports aliases, and all 9 are assessed.</li>
  <li><strong>Docker mounts were invisible.</strong> For containerised servers, what the agent can reach is set by <code class="language-plaintext highlighter-rouge">-v</code> and <code class="language-plaintext highlighter-rouge">--mount</code> flags, not by the server’s arguments. The scanner now reads host bind mounts as the grant’s scope, so mounting your home directory gets flagged, and read-only mounts are treated as read-only.</li>
</ol>

<p>Testing your own tool against real-world input is the cheapest audit you’ll ever run.</p>

<h2 id="what-to-do-about-it">What to do about it</h2>

<ol>
  <li><strong>Pin every server.</strong> Use a version for npm and <code class="language-plaintext highlighter-rouge">uvx</code>, and a digest for Docker images. Treat MCP servers like any other dependency.</li>
  <li><strong>Keep secrets out of config files.</strong> Reference an environment variable or a secret manager, never a literal token.</li>
  <li><strong>Review the combination, not the server.</strong> Ask the exfiltration question for the whole agent: can it read something sensitive, ingest untrusted content and send data out?</li>
  <li><strong>Give agents owners and approval dates.</strong> If nobody owns an agent’s access, nobody will ever take it away.</li>
</ol>

<h2 id="caveats">Caveats</h2>

<p>Some MCP clients ask before each tool call. That setting isn’t visible in config files, so the scan reports what the configuration allows. The postgres, puppeteer and slack servers have been archived, but their READMEs are still widely linked and copied. The framework mappings are my own judgment.</p>

<h2 id="reproduce-it">Reproduce it</h2>

<p>Every config links to the exact commit it came from, and the full results are in the <a href="https://github.com/Mihirajmera/agent-access-review/tree/main/case-studies/official-mcp-readmes">case study</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>agent-access-review
agent-access-review scan path/to/your/mcp-configs
</code></pre></div></div>

<p>Run it on your own setup. I’d be surprised if it comes back clean.</p>]]></content><author><name>Mihir Ajmera</name></author><category term="AI agents" /><category term="MCP" /><category term="Access reviews" /><summary type="html"><![CDATA[I scanned the configuration examples from 9 widely used MCP server READMEs. No single one is dangerous. Pasted together, they build an exfiltration path.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mihirajmera.github.io/assets/img/posts/what-mcp-readmes-teach-you-to-copy.png" /><media:content medium="image" url="https://mihirajmera.github.io/assets/img/posts/what-mcp-readmes-teach-you-to-copy.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>