<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Notes by Imran Siddique</title>
    <link>https://imransiddique.com/notes/</link>
    <atom:link href="https://imransiddique.com/notes/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Working notes on agent security, evidence and governance.</description>
    <language>en</language>
    <dc:creator>Imran Siddique</dc:creator>
    <lastBuildDate>Mon, 07 Sep 2026 19:05:35 +0000</lastBuildDate>
    <item>
      <title>The fix shipped three weeks before the bug had a name</title>
      <link>https://imransiddique.com/notes/2026-09-07-filed-under-bug-fixes.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-09-07-filed-under-bug-fixes.html</guid>
      <pubDate>Mon, 07 Sep 2026 09:00:00 +0000</pubDate>
      <category>agent-security</category>
      <category>supply-chain</category>
      <category>coding-agents</category>
      <description>One coding agent answered repo-controlled git config with a confirmation prompt in August. Another answered the neighbouring case with a refusal. The gap between those two choices is the whole argument, and neither public record tells you which one you are running.</description>
      <content:encoded><![CDATA[<p><em>One coding agent answered repo-controlled git config with a confirmation prompt in August. Another answered the neighbouring case with a refusal. The gap between those two choices is the whole argument, and neither public record tells you which one you are running.</em></p>
<p>On 10 August, Qwen Code shipped v0.21.9. One line in its changelog, filed under Bug Fixes:</p>
<blockquote><p>Requires explicit confirmation for read-only Git commands when repository configuration executes external programs via <code>diff.external</code> or <code>core.fsmonitor</code>.</p></blockquote>
<p>Three weeks later that shape of bug got a name and a list of affected coding agents.</p>
<p>The mechanism is not in dispute. Two CVE records describe it in the same terms. In goose, a repository whose <code>.git/config</code> sets <code>core.fsmonitor</code> to a command causes git to run that command during the index refresh that <code>git diff HEAD</code> performs, before the agent contacts a model at all. In Hermes Agent, the same key fires on the <code>git status</code> refresh triggered by sending any message. Both were fixed. Neither fix required the researchers to be clever; the repository simply supplies part of the command line.</p>
<h2>Two vendors, one bug class, two different controls</h2>
<p>Qwen's answer is a confirmation prompt. Claude Code's changelog gives the other answer, from a neighbouring case. In v2.1.196 there is exactly one line marked Security:</p>
<blockquote><p><code>claude mcp list</code>/<code>get</code> no longer spawn <code>.mcp.json</code> servers that a repo self-approved via a committed <code>.claude/settings.json</code>; untrusted workspaces show Pending approval</p></blockquote>
<p>Same class. A file committed inside a repository causes the agent to spawn a process during a command whose name suggests it only reads. <code>list</code> and <code>get</code> sound as passive as <code>status</code> and <code>diff</code>. The control chosen is different. It does not ask the developer whether this repository should be allowed to configure itself. It refuses, and marks the workspace pending.</p>
<p>That gap is the argument. A confirmation prompt asks a developer to adjudicate <code>core.fsmonitor</code> in a repository they opened ninety seconds ago, before they have read a line of it, while the thing they wanted was to start work. Everything needed to answer the question sits inside the artifact the question is about. Prompts of that shape get approved, and the people most likely to approve them are the ones who have seen the prompt before and had it be fine every previous time. A prompt is a control that degrades with familiarity. The control that holds is the one that never asks: run git with the repository's values for those keys unset, and the question does not arise.</p>
<h2>What the public record cannot tell you</h2>
<p>Coverage credits Claude Code v2.1.196 with fixing the <code>core.fsmonitor</code> issue. Two records a careful user would check disagree by saying nothing at all.</p>
<p>The changelog is silent. I grepped the published file, all 6,362 lines. <code>fsmonitor</code> does not appear. Neither does <code>diff.external</code>, nor the string <code>core.</code> anywhere in the file.</p>
<p>The advisory database is also silent. goose and Hermes both have CVE records naming this exact mechanism. For Claude Code there is no advisory published in August or September 2026, and none listing a fixed version anywhere in the 2.1.19x range. The most recent is from 24 July.</p>
<p>That is not evidence that no fix shipped. Silent fixes are ordinary and often correct. It is evidence of something narrower: from the changelog and the advisory database together, a person running v2.1.196 cannot establish whether they have this fix, while a person running goose or Hermes can establish it in one query. Anyone diffing changelogs to decide whether to upgrade gets no answer, and diffing changelogs is what most teams actually do.</p>
<p>Worth adding that <code>.claude/settings.json</code> has been an attacker-controlled trust input before. In March, CVE-2026-33068 described a malicious repository setting <code>permissions.defaultMode</code> to <code>bypassPermissions</code> in that committed file, silently skipping the trust dialog. Fixed in 2.1.53. The September line is the second time the same repo-controlled file has had to be defended against the same idea.</p>
<h2>What did not survive checking</h2>
<p>The researchers' report is not in here. Their domain, and three write-ups of it, were unreachable, so every claim that originates with them stayed out: the name given to the disclosure, the count of affected agents, the tally of unpatched issues, and the specific claim that Qwen Code remains affected at v0.22.3. That last one is why this note does not tell you whether the August prompt held. Nine stable releases separate v0.21.9 from v0.22.3, and I can see the mitigation shipped and cannot see whether it was enough.</p>
<p>One figure was wrong on the way in. The single Security line in v2.1.196 is one of <strong>nine</strong> such lines in that changelog, not seven; the first count matched only the unbolded form and missed two written as <code><strong>Security:</strong></code>.</p>
<p>Pull request 8645 landed on 10 August. Whoever wrote it was not responding to a disclosure, because there was not one yet. They found it, wrote one sentence, and shipped. That sentence then sat in a public changelog, naming both configuration keys, for three weeks, while the same key was still live in other agents. It was never hidden. It was filed under Bug Fixes.</p>
<h2>Checked against</h2>
<ul><li><a href="https://raw.githubusercontent.com/QwenLM/qwen-code/main/CHANGELOG.md">Qwen Code CHANGELOG, v0.21.9 (2026-08-10), the diff.external and core.fsmonitor line, PR 8645</a></li><li><a href="https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md">Claude Code CHANGELOG, v2.1.196, the single line marked Security</a></li><li><a href="https://api.osv.dev/v1/vulns/CVE-2026-72718">CVE-2026-72718, goose CLI arbitrary command execution via git core.fsmonitor, fixed 1.44.0</a></li><li><a href="https://api.osv.dev/v1/vulns/CVE-2026-71963">CVE-2026-71963, Hermes Agent RCE via git core.fsmonitor config injection</a></li><li><a href="https://api.osv.dev/v1/vulns/GHSA-mmgp-wc2j-qcv7">CVE-2026-33068, Claude Code workspace trust dialog bypass via repo-controlled .claude/settings.json, fixed 2.1.53</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>The finding was correct and eight weeks late</title>
      <link>https://imransiddique.com/notes/2026-09-02-already-said-eight-weeks-earlier.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-09-02-already-said-eight-weeks-earlier.html</guid>
      <pubDate>Wed, 02 Sep 2026 09:00:00 +0000</pubDate>
      <category>attestation</category>
      <category>tls</category>
      <category>spec-review</category>
      <description>A gap in the SEAT post-handshake attestation draft turned out to have been argued on the working group list in July, and specified in a closed issue a year before that. What I filed instead came from reading the RFCs the draft already cites.</description>
      <content:encoded><![CDATA[<p><em>A gap in the SEAT post-handshake attestation draft turned out to have been argued on the working group list in July, and specified in a closed issue a year before that. What I filed instead came from reading the RFCs the draft already cites.</em></p>
<p>The gap I had found was real, and somebody had already said so. Nathanael Ritz put it to the SEAT mailing list on 10 July, quoting the same sentence I had marked, making the argument in more detail than I had, and grounding it on the working group charter rather than on the document's abstract. Two people replied substantively the same day. My contribution would have been to say it again, later, and worse, to the people who had already had the conversation.</p>
<p>That is the case for checking prior art before you verify anything, rather than after. Verification is the expensive step and it answers the second question.</p>
<h2>The document</h2>
<p><code>draft-fossati-seat-expat</code> moves remote attestation out of the TLS handshake and into RFC 9261 exported authenticators, after intra-handshake attestation was shown to permit relay attacks. Its own appendix gives the motive: the older design "does not bind the Evidence to the application traffic secrets, resulting in relay attacks".</p>
<h2>What I nearly filed</h2>
<p>The abstract promises the approach "supports both the passport and background check models" while attestation "remains bound to the underlying communication channel". The Terminology section defines "attestation credentials" as covering both Evidence and attestation results. Every binding requirement in the document then names Evidence and only Evidence. A word-boundary grep for <code>result</code> across the binding section returns zero. The passport branch's sole requirement is that the Attestation Result is correctly signed and meets policy.</p>
<p>All true, and all previously observed. Ritz quoted that same passport bullet in July. Worse for my version, a comment on issue 21, closed, filed under the title "Misleading Finished Message" and ostensibly about a diagram, had already asked "What binds the attestation results to the handshake?" in August 2025 and specified the fix: carry <code>certificate_request_context</code> to the Verifier as <code>rdata</code> and have it reflected into the Attestation Result. Running <code>git log -S rdata</code> over the draft source returns nothing. The mechanism was designed, recorded in a thread about a figure, and lost.</p>
<h2>What I filed instead</h2>
<p>The draft specifies the TLS exporter label twice, and spells it differently each time. Section 4 says the verifier recomputes the exporter value "using the label Attestation Binding". Section 5.1 says the invocation uses "the label Attestation", and prints <code>TLS-Exporter("Attestation", certificate_request_context, 32)</code>. The label is an input to <code>Derive-Secret</code>, so two implementations that each follow one section derive different values and every binding check between them fails. Both spellings are in the published -03.</p>
<p>The part that makes it more than an editorial nit came from the draft's own citations. RFC 8446 section 7.5 says requirements for exporter label format are defined in section 4 of RFC 5705. That section says all label values "MUST be registered via Specification Required". Section 6 adds that IANA "MUST also verify that one label is not a prefix of any other label". <code>Attestation</code> is a prefix of <code>Attestation Binding</code>, so the two can never both be registered, whichever section the working group prefers. Neither is registered today; the draft's IANA section registers only the extension type, and the string <code>attestation</code> does not appear anywhere in the TLS parameters registry.</p>
<h2>What did not survive checking</h2>
<p>The novelty of the lead, entirely, for the reasons above. The claim was verified and the finding was spent.</p>
<p>Also the assumption underneath it. I had been treating the requirement that Evidence carry a hash of the authenticator identity key as settled ground, the fixed half against which the passport half looked unfinished. Issue 58 is an open challenge to exactly that requirement, asking why the public key needs hashing into the binder at all when the exported key material is already bound to the session, with a co-author answering that he cannot say more until the formal analysis is done. Building on it as a solved baseline would have walked into a live dispute.</p>
<p>Two counts were wrong in the version I started from: the file is 35,398 bytes rather than 34,900, and "Attestation Result" occurs 22 times rather than 20. Neither touches the load-bearing claim, which is that none of those occurrences falls in the binding or freshness sections.</p>
<p>If you are reading an unfamiliar draft, the two greps that pay are these. Take every value it defines and check it against the registry that owns that namespace. Then take every specification it cites normatively and read what that document actually requires. Both are cheap, both produce findings its authors have already agreed to be bound by, and neither asks you to know anything they do not.</p>
<h2>Checked against</h2>
<ul><li><a href="https://github.com/tls-attestation/exported-attestation/blob/main/draft-fossati-seat-expat.md">draft-fossati-seat-expat, source on GitHub</a></li><li><a href="https://mailarchive.ietf.org/arch/msg/seat/Xwf2c1jfUWeGgHjWGFliKqtqANM/">SEAT list, The Passport Model-sized elephant in the room, 10 July 2026</a></li><li><a href="https://github.com/tls-attestation/exported-attestation/issues/21">exported-attestation issue 21, comment of 4 August 2025</a></li><li><a href="https://github.com/tls-attestation/exported-attestation/issues/58">exported-attestation issue 58, why the public key is hashed into the binder</a></li><li><a href="https://www.rfc-editor.org/rfc/rfc8446.html#section-7.5">RFC 8446 section 7.5, Exporters</a></li><li><a href="https://www.rfc-editor.org/rfc/rfc5705.html#section-4">RFC 5705 sections 4 and 6, exporter label format and registry</a></li><li><a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml">IANA TLS Parameters, Exporter Labels registry</a></li><li><a href="https://github.com/tls-attestation/exported-attestation/issues/63">exported-attestation issue 63, filed 2 September 2026</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>Two normative sentences went missing when one tasks spec superseded another</title>
      <link>https://imransiddique.com/notes/2026-08-31-two-sentences-lost-in-supersession.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-31-two-sentences-lost-in-supersession.html</guid>
      <pubDate>Mon, 31 Aug 2026 09:00:00 +0000</pubDate>
      <category>mcp</category>
      <category>agent-security</category>
      <category>spec-review</category>
      <description>The MCP tasks extension requires an authorization check on every task request. Its own rationale says that check is often impossible, the sentence that used to make servers disclose that is gone, and no error code represents a denial. One of the two losses was already found and fixed by somebody else three weeks ago.</description>
      <content:encoded><![CDATA[<p><em>The MCP tasks extension requires an authorization check on every task request. Its own rationale says that check is often impossible, the sentence that used to make servers disclose that is gone, and no error code represents a denial. One of the two losses was already found and fixed by somebody else three weeks ago.</em></p>
<p>The MCP tasks extension defines three protocol error codes. <code>-32602</code> for an invalid or nonexistent task ID, <code>-32603</code> for internal errors, <code>-32021</code> for a missing client capability. Its Security Considerations require servers to perform an authorization check on every task-related request. None of the three codes represents that check failing.</p>
<p>So a server that runs the mandated check and denies has nothing to return that says it denied. It returns <code>-32602</code>, which is also the answer for a task that never existed. A denial and a typo are the same response.</p>
<p>That may be deliberate. There is a decent anti-enumeration argument for it, and the extension has no <code>tasks/list</code> for related reasons. But it is written down nowhere, so nobody implementing to the spec can tell a design decision from a hole.</p>
<p>I filed that and one other point as issue 20 this afternoon. Checking whether they were already raised is most of what follows, and it changed what I filed.</p>
<h2>The part that took reading two repositories</h2>
<p>The requirement and its rationale do not live in the same document.</p>
<p>The Security Considerations bullet is in <code>ext-tasks</code>, the extension's own repo. The paragraph explaining when it can actually be satisfied is in SEP-2663, in the main specification repo, and says this:</p>
<blockquote><p>all tasks should be bound to some sort of "authorization context," the implementation of which is left to individual servers according to their existing bespoke permission models. However, in many cases, it is not possible to perform this binding, in which case the task ID becomes the only line of defense against contamination.</p></blockquote>
<p>With sessions removed from the protocol by SEP-2567, there is, in the SEP's words, "no other natural scope a server can define unilaterally". <code>grep -ci motivation</code> on the ext-tasks specification returns zero. An implementer working from the extension repo sees a MUST with none of the context that bounds it.</p>
<p>In those deployments the mandated check reduces to the bearer check the neighbouring bullet permits, where a server MAY use task IDs as bearer tokens. Possession of the handle becomes the authorization. That is a different property from the one the MUST describes, and the document uses the vocabulary of the stronger one.</p>
<h2>Somebody already found half of this</h2>
<p>The auth binding bullet was not in the extension at all until eleven days ago.</p>
<p>Rich Smith opened PR 9 on 31 July, titled "restore auth binding requirement dropped during SEP-2663 port". It was merged on 20 August. His argument: the port carried three of the four Security Implications bullets and dropped the second, leaving <code>tasks/get</code>, <code>tasks/update</code> and <code>tasks/cancel</code> with no specified authorization requirement. In his words, an unguessable task ID was the only control the specification mandated.</p>
<p>He was right and it is fixed. I did not expect the second loss underneath it.</p>
<p>SEP-1686, the tasks proposal this extension supersedes, had a rule for exactly the case the Motivation paragraph describes:</p>
<blockquote><p>Receivers that do not implement session or authentication binding SHOULD document this limitation clearly, as task results may be accessible to any requestor that can guess the task ID.</p></blockquote>
<p>That sentence is not in SEP-2663 and not in the extension. The requirement to bind survived the transition, after being restored by hand. The requirement to disclose when you cannot bind did not survive, and nobody has restored it.</p>
<p>Two normative sentences from the same section went missing in the same supersession. One was caught by a contributor doing a bullet-by-bullet diff. The other is still gone.</p>
<h2>What did not survive checking</h2>
<p>The framing I started with was that the document contradicts itself. It does not, because the two halves are in different repositories, and on the repo where the requirement lives the rationale is simply absent. That is a weaker claim and a more useful one: this is a porting loss, not a drafting error, and it has the same signature as the one already fixed.</p>
<p>I also could not claim the error taxonomy point was novel. The same shape was raised during SEP-2663 review for <code>requestState</code> integrity failures, and the maintainers resolved it on 4 May by allowing a synchronous error path as SHOULD statements. That is why two lines in Protocol Errors read SHOULD today. The authorization case is the one that conversation did not cover.</p>
<p>And the Agents WG settled the governing principle three days ago, on 28 August, for a different proposal: operation-specific error codes should be defined, reusing an existing protocol code where an appropriate one exists. Pointing at a decision a group has already made is worth more than arriving with an argument they have not asked for.</p>
<h2>The check worth stealing</h2>
<p>When a specification says it supersedes another, diff the security sections rather than reading the new one. Two separate losses came out of one transition here, and the one that was caught was caught by somebody comparing bullets by hand.</p>
<p>The roadmap published on 22 August wants servers recognising agent identities built on existing standards "rather than pasted API keys and long-lived tokens", and names maturing this extension so it can move into the core specification. In the draft schema <code>ttlMs</code> is <code>number | null</code>, null documented as unlimited. A handle that may be a bearer token and need never expire is worth reconciling with that sentence while the document is cheap to change.</p>
<h2>Checked against</h2>
<ul><li><a href="https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md">SEP-2663 Tasks Extension, merged 15 May 2026</a></li><li><a href="https://github.com/modelcontextprotocol/ext-tasks/blob/main/specification/draft/tasks.md">ext-tasks specification/draft/tasks.md, Security Considerations and Protocol Errors</a></li><li><a href="https://github.com/modelcontextprotocol/ext-tasks/pull/9">ext-tasks PR 9, restore auth binding requirement dropped during SEP-2663 port</a></li><li><a href="https://github.com/modelcontextprotocol/agents-wg/blob/main/proposals/1686-tasks.md">SEP-1686 tasks proposal, section 8.1 Task Isolation and Access Control</a></li><li><a href="https://github.com/modelcontextprotocol/agents-wg/blob/main/meetings/2026-08-28.md">Agents WG meeting notes, 28 August 2026, error contracts for agent operations</a></li><li><a href="https://github.com/modelcontextprotocol/ext-tasks/blob/main/schema/draft/schema.ts">ext-tasks schema/draft/schema.ts, Task interface and ttlMs</a></li><li><a href="https://blog.modelcontextprotocol.io/posts/mcp-roadmap/">MCP roadmap, 22 August 2026</a></li><li><a href="https://github.com/modelcontextprotocol/ext-tasks/issues/20">ext-tasks issue 20, filed 31 August 2026</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>The tool list was advertised, the resolver decided</title>
      <link>https://imransiddique.com/notes/2026-08-24-tool-list-advertised-resolver-decided.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-24-tool-list-advertised-resolver-decided.html</guid>
      <pubDate>Mon, 24 Aug 2026 09:00:00 +0000</pubDate>
      <category>agent-security</category>
      <category>tool-calling</category>
      <category>enforcement</category>
      <description>Spring AI released a fix on 21 August for a bug where a tool absent from the request could still be called. The vulnerable dispatch is one line. The public fix exists on one of the three affected release lines, and the fallback left almost nothing useful in the log.</description>
      <content:encoded><![CDATA[<p><em>Spring AI released a fix on 21 August for a bug where a tool absent from the request could still be called. The vulnerable dispatch is one line. The public fix exists on one of the three affected release lines, and the fallback left almost nothing useful in the log.</em></p>
<p>Here is the vulnerable path in <code>DefaultToolCallingManager</code> at tag <code>v2.0.0</code>:</p>
<pre><code>ToolCallback toolCallback = toolCallbacks.stream()
    .filter(tool -&gt; toolName.equals(tool.getToolDefinition().name()))
    .findFirst()
    .orElseGet(() -&gt; this.toolCallbackResolver.resolve(toolName));</code></pre>
<p><code>toolCallbacks</code> is the list attached to this request. It is also the list advertised to the model.</p>
<p>If the model returns a name that is absent from that list, execution does not stop. The manager falls through to a resolver that has no knowledge of the request.</p>
<p>Spring's advisory describes the distinction precisely: the per-request tool list was advertised as a boundary but was not fully enforced during dispatch.</p>
<p>Sit with the word <em>advertised</em>. The boundary was something said to the model. The thing that actually decided was a lookup in a map.</p>
<h2>What the map holds</h2>
<p><code>StaticToolCallbackResolver</code> holds a map keyed by tool name. Its <code>resolve</code> operation is a lookup in that map. <code>DelegatingToolCallbackResolver</code> walks its configured resolvers and returns the first non-null result. Neither carries the request's advertised tool set.</p>
<p>For the built-in resolvers, the per-request list governed what the model could see. The resolver governed what the process could execute.</p>
<p>The evidence gap follows the enforcement gap. A successful fallback produced no fallback-specific log in the manager. The static resolver emits a debug message, but the message does not include the tool name.</p>
<p>Enforcement did not run. Separately, the evidence needed to reconstruct the bypass was not written either. An audit log cannot tell you which unadvertised tool was resolved if the resolution event never names it.</p>
<h2>The shape of the fix</h2>
<p>At <code>v2.0.1</code>, the dispatch line became:</p>
<pre><code>.orElseGet(() -&gt; this.resolutionFallbackEnabled
        ? this.toolCallbackResolver.resolve(toolName)
        : null);</code></pre>
<p>The default is now <code>false</code>.</p>
<p>That is the right default for a library. It is still worth naming what kind of control it is: an in-process default that an application can reverse.</p>
<p>Spring documents two ways to restore the old behaviour. Direct users can set <code>.resolutionFallbackEnabled(true)</code> on the builder. Spring Boot users can set:</p>
<pre><code>spring.ai.tools.resolution.fallback.enabled=true</code></pre>
<p>Dynamic tool resolution is a legitimate requirement. But once fallback is restored, the request's advertised list is no longer the complete execution boundary. That choice needs its own policy and evidence, not merely a configuration value.</p>
<h2>Which line got the public fix</h2>
<p>The advisory identifies three affected release lines:</p>
<ul><li><code>1.0.0</code> through <code>1.0.9</code></li><li><code>1.1.0</code> through <code>1.1.8</code></li><li><code>2.0.0</code></li></ul>
<p>Its fix table contains four versions. <code>2.0.1</code> is marked OSS. <code>2.0.0.1</code>, <code>1.1.9</code>, and <code>1.0.10</code> are marked Enterprise Support Only.</p>
<p>That is a published commercial support model doing what it says it does. It is not a scandal. It does change the usual "upgrade to 2.0.1" summary.</p>
<p>For someone already on <code>2.0.0</code>, the public fix is a patch bump. For someone on either affected 1.x line, the publicly available route is a major-version migration. The advisory's table says that plainly.</p>
<p>I also dropped the CVSS number. The vector published by Spring did not reconcile with one score presented during checking, so this note uses Spring's <strong>Medium</strong> severity label and makes no numerical claim.</p>
<h2>Where I sit in it</h2>
<p>cMCP enforces policy where tool calls cross its gateway. Its limitations already say that the gateway controls the tool boundary, not the model boundary, and that protection does not apply when a call bypasses the gateway.</p>
<p>A callback resolved from a local Java registry is an in-process method call. It never crosses an MCP boundary. My gateway would not see it, deny it, or place it in its audit chain.</p>
<p>That does not make the gateway control wrong. It identifies its enforcement domain.</p>
<p>A control plane that observes calls leaving a process governs one class of tool execution. CVE-2026-59318 lived in the other class. I do not know how many deployed applications re-enable the fallback or otherwise resolve tools inside the process.</p>
<h2>Checked against</h2>
<ul><li><a href="https://spring.io/security/cve-2026-59318/">Spring advisory CVE-2026-59318, published 20 August 2026</a></li><li><a href="https://spring.io/blog/2026/08/21/spring-ai-2-0-1-available-now/">Spring AI 2.0.1 release announcement, 21 August 2026</a></li><li><a href="https://docs.spring.io/spring-ai/reference/upgrade-notes.html#_tool_resolution_fallback_disabled_by_default">Spring AI 2.0.1 upgrade notes, tool resolution fallback</a></li><li><a href="https://github.com/spring-projects/spring-ai/blob/v2.0.0/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java">DefaultToolCallingManager at v2.0.0, the vulnerable fallback</a></li><li><a href="https://github.com/spring-projects/spring-ai/blob/v2.0.1/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java">DefaultToolCallingManager at v2.0.1, the disabled-by-default fallback</a></li><li><a href="https://github.com/spring-projects/spring-ai/blob/v2.0.1/spring-ai-model/src/main/java/org/springframework/ai/tool/resolution/StaticToolCallbackResolver.java">StaticToolCallbackResolver at v2.0.1, registry lookup and debug log</a></li><li><a href="https://repo1.maven.org/maven2/org/springframework/ai/spring-ai-model/">Maven Central, public spring-ai-model versions</a></li><li><a href="https://github.com/agentrust-io/cmcp/blob/main/LIMITATIONS.md">cMCP LIMITATIONS.md, gateway enforcement boundary</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>The MCP registry checks that the repository URL looks like GitHub</title>
      <link>https://imransiddique.com/notes/2026-08-21-repository-url-looks-like-github.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-21-repository-url-looks-like-github.html</guid>
      <pubDate>Fri, 21 Aug 2026 09:00:00 +0000</pubDate>
      <category>agent-security</category>
      <category>provenance</category>
      <category>registries</category>
      <description>Publishing proves you own the namespace and you own the package. The repository URL next to them gets a regex, 498 entries name a repo owned by someone other than the publisher, and the one a vendor flagged twelve days ago is still marked active.</description>
      <content:encoded><![CDATA[<p><em>Publishing proves you own the namespace and you own the package. The repository URL next to them gets a regex, 498 entries name a repo owned by someone other than the publisher, and the one a vendor flagged twelve days ago is still marked active.</em></p>
<p>The official MCP registry holds 24,615 server records. Of the 16,850 published under an <code>io.github.*</code> namespace, 15,235 name a GitHub repository, and 498 of those name a repository owned by a different GitHub account than the namespace the publisher authenticated as.</p>
<p>Most of those 498 are almost certainly innocent. A personal account publishing an organisation's code, a monorepo, an account rename. That is the point. Nothing checked, so nothing distinguishes them.</p>
<h2>What publishing actually proves</h2>
<p>The requirements document is clear and short. Four validations: namespace authentication, package ownership verification, restricted registry base URLs, and <code>_meta</code> namespace restrictions. Grep it for "repositor" and you get nothing.</p>
<p>The code is not silent, though, and the difference matters. <code>validateRepository</code> calls <code>IsValidRepositoryURL</code>, which is this:</p>
<pre><code>^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$</code></pre>
<p>A shape check. Not whether the repository exists, not whether it resolves, not whether the publisher owns it, not whether it has anything to do with the package sitting beside it in the same record. If the field is absent the validator returns early, which is how 5,258 entries have no repository at all.</p>
<p>So the record proves two things and asserts a third. The namespace is proven. The package is proven. The repository is typed in.</p>
<p>None of which is news to the maintainers. Issue 395 was opened in September 2025 by someone who found a live entry pointing at a GitHub URL that would not load, and the first reply, from a maintainer, is that the registry should "go one step further" and validate that the repository is "not only valid/publicly accessible but also actually owned by the publisher". Eleven months later the thread has eight comments, the URL in question turned out to be a private repo rather than a broken link, and PR 1266 has had a publish-time reachability probe waiting since May. The check the maintainer asked for on day one has not been built.</p>
<h2>What used the gap</h2>
<p>On 8 August at 20:58 UTC, <code>io.github.jUXTAPOSITION1/vape</code> was published. On 9 August, one day later, OX Security reported it. Their account: the PyPI package is clean, deliberately, to get past automated scanners, and the payload sits in the linked repository in <code>.vscode/settings.json</code> and <code>.claude/settings.json</code>, which run when a developer opens or clones the project in a coding client. They describe it as the first time they had observed this worm delivered through the official registry.</p>
<p>Every prior version of this story ran the other way. Honest repository, poisoned package. Here the clean package is the alibi and the pointer is the weapon, and the pointer is the field that gets the regex.</p>
<p>GitHub blocked the repository for terms-of-service violation on 15 August. The PyPI package is still installable, both releases, neither yanked. The registry entry is still <code>active</code>, and its <code>statusChangedAt</code> is identical to the microsecond to its <code>publishedAt</code>.</p>
<h2>Why nobody attached a warning</h2>
<p>There are three statuses. <code>active</code>, visible by default. <code>deprecated</code>, visible with a message. <code>deleted</code>, hidden by default. <code>statusMessage</code> is capped at 500 characters and rejected with a 400 when the status is <code>active</code>.</p>
<p>I read all 735 status messages in the registry. Not one describes a moderation action. They are renames, consolidations, accidental version bumps, republications under a new namespace. That is not a failure, it is the feature working as designed: <code>statusMessage</code> was introduced to carry a publisher's deprecation reason, and issue 623 was closed on exactly that ground.</p>
<p>Which leaves the vocabulary with no way to say the thing this entry needs said. An aggregator mirroring the feed cannot tell a publisher's rename from a registry takedown, because both are a status plus free text, and there is no actor on the record.</p>
<h2>What did not survive checking</h2>
<p>"The registry does not validate the repository field." Cut. It validates the shape. One grep would have refuted the stronger claim in public.</p>
<p>"The choice is silence or removal." Cut. <code>deprecated</code> is documented as visible with a warning. The measured claim replaces it: the warning tier exists and has never once been used for this.</p>
<p>"The repository was deleted." Corrected. GitHub's API records it as blocked for terms of service since 15 August, which is a stronger fact than the 404 I started from.</p>
<p>"The maintainers deferred ownership binding." Cut, and it was nearly the worst error here. That is PR 1266's characterisation of the thread. The thread itself has a maintainer asking for ownership validation in the first reply. Nobody decided against it.</p>
<p>I have not run the payload or read the repository, which is disabled. The attribution is OX Security's and I am reporting it as theirs.</p>
<h2>Where I sit in this</h2>
<p>cMCP's own limitations file says the gateway cannot detect a look-alike package added to the catalog in the first place, because catalog approval is human-gated. This is a case study in what that human is handed: a green status, a proven package name, and a repository URL that was checked for punctuation.</p>
<p>Filed today as issue 1563, a takedown request against the entry, citing the moderation policy's malware clause. The reachability gap is already in 395 and 1266 and does not need me to file it again.</p>
<h2>Checked against</h2>
<ul><li><a href="https://registry.modelcontextprotocol.io/v0/servers?search=vape">MCP registry, the io.github.jUXTAPOSITION1/vape entry, read 21 August 2026</a></li><li><a href="https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/official-registry-requirements.md">Official registry requirements, the four validations enforced on publish</a></li><li><a href="https://github.com/modelcontextprotocol/registry/blob/main/internal/validators/validators.go">internal/validators/validators.go, validateRepository</a></li><li><a href="https://github.com/modelcontextprotocol/registry/blob/main/internal/validators/utils.go">internal/validators/utils.go, IsValidRepositoryURL and the two regexes</a></li><li><a href="https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/official-registry-api.md">Official registry API reference, status values and the statusMessage restriction</a></li><li><a href="https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/moderation-policy.mdx">The MCP Registry moderation policy</a></li><li><a href="https://github.com/modelcontextprotocol/registry/issues/395">Issue 395, validate provided repositories are publicly reachable, open since September 2025</a></li><li><a href="https://github.com/modelcontextprotocol/registry/pull/1266">PR 1266, probe repository URL reachability at publish</a></li><li><a href="https://www.ox.security/blog/shai-hulud-outbreak-debrief-the-worm-evolves-into-mcp/">OX Security, Shai-Hulud outbreak debrief, 9 August 2026</a></li><li><a href="https://pypi.org/pypi/vape-mcp-server/json">PyPI JSON API, vape-mcp-server</a></li><li><a href="https://github.com/agentrust-io/cmcp/blob/main/LIMITATIONS.md">cMCP LIMITATIONS.md, tool name collision via malicious catalog entries</a></li><li><a href="https://github.com/modelcontextprotocol/registry/issues/1563">Issue 1563, the takedown request I filed against the entry</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>The platform bit that means the opposite of what the doc says</title>
      <link>https://imransiddique.com/notes/2026-08-20-platform-bit-opposite.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-20-platform-bit-opposite.html</guid>
      <pubDate>Thu, 20 Aug 2026 09:00:00 +0000</pubDate>
      <category>attestation</category>
      <category>sev-snp</category>
      <category>evidence</category>
      <description>Google Cloud tells you to update go-sev-guest so your parser stops breaking on v4 attestation reports. I wanted to know what you can actually check once it parses, and the answer sent me to look at my own verifier.</description>
      <content:encoded><![CDATA[<p><em>Google Cloud tells you to update go-sev-guest so your parser stops breaking on v4 attestation reports. I wanted to know what you can actually check once it parses, and the answer sent me to look at my own verifier.</em></p>
<p>I set out to answer a small question this morning, and it cost me more than I expected.</p>
<p>Google Cloud's Confidential VM release notes, in an entry dated 27 October 2025, say that after a firmware update SEV-SNP instances generate v4 attestation reports and that parsers written for v3 might break. The fix offered is to update go-sev-guest to v0.14.0 or above. That advice is correct. I wanted to know what you can check once the thing parses.</p>
<h2>The version the advice names cannot check what it reads</h2>
<p>The v4 report added two mitigation vectors, <code>LAUNCH_MIT_VECTOR</code> and <code>CURRENT_MIT_VECTOR</code>, at offsets 0x1F8 and 0x200. They carry which platform-level mitigations were in force at launch and which are in force now.</p>
<p>At v0.14.0, tagged 9 October 2025, the <code>abi</code> package reads both. The <code>validate</code> package does not contain the string "mitigation" anywhere. There is no option to require anything of those vectors and no function that looks at them. Same at v0.14.1.</p>
<p>The policy hooks arrive in v0.15.0, tagged 9 June 2026 and still the latest release: <code>MinimumLaunchMitigationVector</code>, <code>MinimumCurrentMitigationVector</code>, and a check wired into the main validation path.</p>
<p>That is not a scandal. Libraries grow, and the option did not exist anywhere in the interval, so nobody was exposed by choosing wrongly. But "update to v0.14.0 or above" answers the question people asked, which was why their parser crashed, and not the question worth asking, which is what the new report says and whether anyone is reading it.</p>
<h2>Four of seven fields mean the opposite of the documentation</h2>
<p>The second half is a documentation problem rather than a code one.</p>
<p>The library's platform-info policy field is documented as a ceiling. The doc comment says <code>PlatformInfo</code> is the maximum of acceptable data. The README says each true field is permission for the corresponding report bit to be set. Read that and you build a policy by deciding what you will tolerate.</p>
<p>Then read the function. The struct has seven fields. Three behave that way: SMT, TSME and SEV-TIO all fail with "unauthorized" when the report carries a bit your policy did not grant.</p>
<p>The other four are reversed. ECC, RAPL disabled, ciphertext hiding and alias-check-complete all fail when the report is <em>missing</em> something your policy asked for. For those, setting the bit true is not permission. It is a demand.</p>
<p>The one I care about is alias-check-complete. The comment above it in the source names what it is for and links to BadRAM and AMD's bulletin SB-3015. The firmware's alias check is the thing that prevents. The bit in the report only proves the firmware ran it. A verifier can require that proof, and under the documented model an operator has no reason to, because they are not trying to authorise anything. They leave it false and the requirement never runs.</p>
<h2>What did not survive checking</h2>
<p>The morning brief that pointed me here claimed nobody had made this argument before. Wrong. Issue 187 has been open since 16 April, unanswered, reporting the same confusion from the other end: that TSME can only be required to be off. I read that as evidence for the doc being the cause, and cited it.</p>
<p>It also had three of the four error strings wrong, and counted thirteen references where there are fifteen. I could not treat any of it as read.</p>
<p>I could not date the release note from search results either. It turned out to be ten months old, which is worth knowing before deciding anything is news.</p>
<h2>What this cost me</h2>
<p>I went to check my own verifier before writing a word of this, and I did not enjoy the result.</p>
<p><code>_snp_verify.py</code> in agent-manifest does not parse <code>PLATFORM_INFO</code> at all. The field sits at offset 0x40. My offset table goes from the signature algorithm at 0x34 straight to report data at 0x50. It is not in the table, not in the parsed struct, and across every first-party file in agent-manifest, cmcp, ca2a and trace-spec the terms <code>platform_info</code>, <code>alias_check</code>, <code>mit_vector</code> and <code>badram</code> appear zero times each.</p>
<p>So this is not a case of my documents failing to mention a check that the code performs. The bytes are never read. What my verifiers do establish is real and fail-closed: report signature, the VCEK to ASK to ARK chain with the root pinned by the operator, measurement binding. That is authenticity and identity. None of those four asks the report what kind of machine it came from.</p>
<p>I have written the honest limit the same way for months, that no confidential computing silicon is custody grade against an adversary who owns the machine, with BadRAM as the reason. I treated it as a boundary to state and live with. Part of it is a boundary you can assert on, and AMD put the bit in the report to let you.</p>
<h2>What changed today</h2>
<p>I filed issue 195 against go-sev-guest, proposing the doc comment and the README bullet be split into the ceiling set and the floor set, with the fields named in each.</p>
<p>Parsing <code>PLATFORM_INFO</code> and exposing an appraisal policy over it goes into agent-manifest next, and the limitations files across all four repos get a line naming what is not checked. Not everything unenforced is unenforceable. Sometimes the check is sitting in the struct and nobody wired it up.</p>
<h2>Checked against</h2>
<ul><li><a href="https://github.com/google/go-sev-guest/blob/c930ed67bebfe7245c0309888ec185bd9ad35899/validate/validate.go">google/go-sev-guest validate.go at c930ed67, read 20 August 2026</a></li><li><a href="https://github.com/google/go-sev-guest/blob/c930ed67bebfe7245c0309888ec185bd9ad35899/abi/abi.go">google/go-sev-guest abi.go at c930ed67, the SnpPlatformInfo struct</a></li><li><a href="https://github.com/google/go-sev-guest/issues/195">Issue 195, the doc bug I filed against go-sev-guest</a></li><li><a href="https://github.com/google/go-sev-guest/issues/187">Issue 187, milesdai on TSME polarity, open since April 2026</a></li><li><a href="https://docs.cloud.google.com/confidential-computing/confidential-vm/docs/release-notes">Google Cloud Confidential VM release notes, entry of 27 October 2025</a></li><li><a href="https://badram.eu/">BadRAM</a></li><li><a href="https://www.amd.com/en/resources/product-security/bulletin/amd-sb-3015.html">AMD security bulletin SB-3015</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>The attestation everyone cites cannot be looked up any more</title>
      <link>https://imransiddique.com/notes/2026-08-19-attestation-you-cannot-look-up.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-19-attestation-you-cannot-look-up.html</guid>
      <pubDate>Wed, 19 Aug 2026 09:00:00 +0000</pubDate>
      <category>supply-chain</category>
      <category>evidence</category>
      <category>provenance</category>
      <description>Thirty poisoned npm packages are gone along with their attestations, and the Sigstore copy that survives can only be fetched with a digest that no longer exists in any public place I could find.</description>
      <content:encoded><![CDATA[<p><em>Thirty poisoned npm packages are gone along with their attestations, and the Sigstore copy that survives can only be fetched with a digest that no longer exists in any public place I could find.</em></p>
<p>Every account of the keyv compromise turns on one sentence. Aikido's is representative: "the poisoned versions were published to npm with valid provenance signed by GitHub Actions." That is the detail that made this story worth writing about rather than another package takeover.</p>
<p>Fifteen days later I went to check it, and at npm you cannot.</p>
<h2>What is left</h2>
<p>I fetched thirty packages this morning: the twenty that one release run published, and ten more across the maintainer's other projects. Every one behaves the same way. The poisoned version is absent from its packument's versions map, its tarball returns 404, and its attestation endpoint returns 404. A control fetch of a clean version returns 200 on all three.</p>
<p>One thing does survive. npm keeps the publish timestamp in the packument's time map for a version that exists nowhere else in the registry. Thirty orphan timestamps, running from 09:30:01.291Z to 10:28:01.451Z on 4 August, and nothing else.</p>
<p>Worth noting in passing, since it bears on every count you have read: the public indicator lists name eleven packages. The release run's own log ends with the line "Published 20 package(s)", from that one repository, before the campaign spread anywhere else. Thirty is a count of what I fetched, not a total.</p>
<h2>The copy that is still there</h2>
<p>Deleting from npm does not delete the attestation. npm's provenance is written to Rekor, Sigstore's public append-only transparency log, and append-only means npm cannot take an entry back out.</p>
<p>The retrieval path works, and I checked that it works before concluding anything about the missing ones. keyv 6.0.0-rc.1, published the evening before the compromise and still live, has a tarball digest in its packument. Handing that sha512 to Rekor's search index returns two entries, which are the two Sigstore bundles npm attached to that release. Fetching one by its log index returns the record.</p>
<p>Now do the same for keyv 6.0.0. The digest lives in the packument's entry for that version, and the packument's entry for that version is exactly what unpublishing removes. The tarball is gone, so I cannot recompute it. npm's attestation endpoint, which would hand it over, is one of the ones returning 404. A public mirror I checked never carried the version at all. Rekor's index also accepts a certificate subject, so I tried the workflow identity instead, and it returned nothing for the poisoned build and nothing for the live control either, which tells me the method does not work rather than that the entry is missing.</p>
<p>So the entry is almost certainly sitting in the log, and I have no way to ask for it.</p>
<h2>Why I think this matters</h2>
<p>Unpublishing was the right call and it did the job it was for. Enforcement runs at the registry, in front of the fetch, and it is what actually stops anyone installing keyv 6.0.0 today. Nothing here argues against it. The point sits next to it, not against it.</p>
<p>Evidence is a different job. It prevents nothing. Its whole value is that somebody who trusts neither the attacker nor the registry can come back later and check a claim. An append-only log answers the question "can this record be removed", which is the question everybody designs for. It does not answer "can this record be found", and that turns out to be the one that bites, because the address was carried by the artifact and the artifact is what you delete. The record outlived its index.</p>
<p>The one other place the build is documented is the workflow run at GitHub. Per GitHub's documented default those logs are deleted 90 days after the run, which for this incident falls in early November, and I could not read the repository's actual setting.</p>
<p>I have not answered this in my own work either. TRACE's limitations document says "Signature validity is permanent; trust is not. Nothing inside a record can retract the key that signed it", and the answer we specified is a revocation store the verifier consults at verification time. That handles a key being withdrawn. It says nothing about a record that stays perfectly valid and becomes unreachable because the thing it describes was deleted, and a content-addressed trust record has the same shape of problem as npm's.</p>
<h2>What did not survive checking</h2>
<ul><li><strong>Whether the poisoned attestations are in Rekor.</strong> I believe they are and I did not confirm it. Both routes failed for the reasons above, and the subject-index route failed its own control, so treat this as unproven rather than as a negative result.</li><li><strong>The size of the campaign.</strong> Aikido's page currently reads 444 packages across 1,381 versions; VentureBeat cites Aikido at 868; JFrog traced more than 400 packages and 1,700 versions. I did not resolve them, so no total appears above and none of the argument rests on one.</li><li><strong>Whether the eleven named packages are the malicious set.</strong> I can show that thirty were published in the window from the same runs and later removed. I cannot show what was in any of them, because the tarballs are gone. That is the same wall as everything else here.</li><li><strong>The 90-day expiry.</strong> That is GitHub's documented default. The repository's configured retention is not readable without access I do not have.</li></ul>
<h2>Checked against</h2>
<ul><li><a href="https://www.aikido.dev/blog/keyv-and-friends-compromised-in-npm-supply-chain-attack">Aikido, &quot;Keyv and friends compromised in npm supply chain attack&quot;, 4 August 2026</a></li><li><a href="https://snyk.io/blog/inside-keyv-npm-compromise-preinstall-malware-trusted-provenance-ide-hooks/">Snyk, &quot;Inside the keyv npm compromise&quot;, read 19 August 2026</a></li><li><a href="https://venturebeat.com/security/the-shai-hulud-npm-worm-didnt-fake-its-security-check-it-earned-a-legitimate-one">VentureBeat, &quot;The Shai-Hulud npm worm didn&#x27;t fake its security check, it earned a legitimate one&quot;</a></li><li><a href="https://nulltap.sh/p/keyv-shai-hulud-provenance/">nulltap, &quot;Keyv&#x27;s signed npm releases carried a credential-stealing worm&quot;</a></li><li><a href="https://registry.npmjs.org/keyv">npm registry packuments and attestation endpoints, read 19 August 2026</a></li><li><a href="https://rekor.sigstore.dev/api/v1/log/entries?logIndex=2336496546">Sigstore Rekor transparency log, search index API</a></li><li><a href="https://docs.github.com/en/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization">GitHub docs, retention period for Actions artifacts and logs</a></li><li><a href="https://github.com/agentrust-io/trace-spec/blob/main/LIMITATIONS.md">TRACE specification, LIMITATIONS.md</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>The LiteLLM compromise is not in any of the places you would look for it</title>
      <link>https://imransiddique.com/notes/2026-08-14-litellm-not-in-the-record.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-14-litellm-not-in-the-record.html</guid>
      <pubDate>Fri, 14 Aug 2026 09:00:00 +0000</pubDate>
      <category>supply-chain</category>
      <category>evidence</category>
      <category>ci-cd</category>
      <description>Both poisoned versions 404 on PyPI, no CVE was ever issued, and the KEV entry filed two days later belongs to the scanner rather than the gateway.</description>
      <content:encoded><![CDATA[<p><em>Both poisoned versions 404 on PyPI, no CVE was ever issued, and the KEV entry filed two days later belongs to the scanner rather than the gateway.</em></p>
<p>In March, two poisoned releases of LiteLLM went up on PyPI. CloudSEK reported on 11 August that they may have reached more than 2,500 companies and 434,000 CI/CD pipelines. On 13 August Hudson Rock published its analysis of a 153GB archive taken in the attack: 433,909 files, of which 118,829 CI runner dumps were attributed to 2,488 corporate domains.</p>
<p>I went to check the figures and found something more interesting than the figures. If you go looking for this incident in the three places an engineer would normally look, it is not in any of them.</p>
<h2>PyPI</h2>
<p><code>litellm</code> 1.82.7 and 1.82.8 are not on PyPI. Not yanked, which would leave them visible and installable by exact pin. Gone. Both return HTTP 404 from PyPI's own API, and neither appears anywhere in an index that currently lists 1,189 versions of the package.</p>
<p>What the record shows instead is 1.82.6 on 22 March, then 1.83.0 on 31 March, with nothing between them and no 1.82.9 at all. A reader arriving at the release history today sees a nine day gap and a minor version bump. Nothing on that timeline says an incident happened.</p>
<p>Removing the artifacts was right. Leaving no marker where they stood is a separate decision, and it is the one that makes the package's own history misleading.</p>
<h2>The CVE</h2>
<p>There is no CVE for the LiteLLM package compromise. Searching NVD across March and April 2026 returns only ordinary code vulnerabilities: an unauthenticated <code>/config/update</code> endpoint, a JWT authentication flaw, a guardrails sandbox escape. LiteLLM appears twice in CISA's Known Exploited Vulnerabilities catalog and both are red herrings, CVE-2026-42208 added on 8 May and CVE-2026-42271 on 8 June, unrelated injection bugs found months later.</p>
<h2>The KEV entry that does exist</h2>
<p>Two days after the poisoned releases there is a KEV entry, and it is worth being precise about whose it is. <strong>CVE-2026-33634, added 26 March, is Aqua Security's Trivy, not LiteLLM.</strong> CVSS 4.0 base score 9.4. CISA's own description: an embedded malicious code vulnerability that could allow an attacker to reach everything in the CI/CD environment, including all tokens, SSH keys, cloud credentials, database passwords and any sensitive configuration in memory.</p>
<p>This matters because Trivy was the way in. CloudSEK describes a leaked automation token that was rotated but not fully revoked, leaving roughly a 20 day window in LiteLLM's release pipeline. The MITRE record for the Trivy CVE is more specific: on 19 March a threat actor published a malicious Trivy v0.69.4, <strong>force-pushed 76 of the 77 version tags in <code>aquasecurity/trivy-action</code></strong> to credential-stealing malware, and replaced all seven tags in <code>aquasecurity/setup-trivy</code>. It was a continuation of an attack that began in late February, and after the 1 March disclosure the credential rotation "was not atomic".</p>
<p>The dates sit close enough that it is easy to file the 26 March listing under LiteLLM and move on, which inverts the lesson. The component that earned a critical CVE and a federal remediation deadline was the scanner inside the build.</p>
<h2>What that does to the usual advice</h2>
<p>The standard response to a poisoned package is to pin your dependencies, and for the PyPI half of this that holds. For the Trivy half it fails in the most literal way available: 76 of 77 tags were force-pushed. Anyone pinned to a version tag got the malware precisely because they were pinned. Only a commit SHA survives a force-push, and almost nobody pins actions by SHA.</p>
<p>Pinning is not integrity. It is a stable name for something somebody else can still overwrite.</p>
<h2>The part I cannot help with</h2>
<p>Five months on, 2,488 organisations established their own exposure by reading someone else's analysis of a stolen archive. Kevin Beaumont tried the credentials belonging to one large US technology company and reported that almost every one still worked.</p>
<p>That gap is not a detection failure. Detection worked, and the packages were live for about forty minutes. It is that nothing produced a durable record of what the build actually did while it ran, so the only surviving account of the incident is the attacker's copy.</p>
<p>I should be straight that this sits outside everything I have built. cMCP records what crosses the gateway and TRACE signs the records so a third party can check them, and neither is anywhere near a CI runner at build time. A poisoned package executing inside your pipeline never touches either one.</p>
<h2>What did not survive checking</h2>
<ul><li><strong>"CISA added the CVE two days in."</strong> Cut. No CVE exists for this compromise. The 26 March KEV entry is Trivy's.</li><li><strong>"Shipped a clean version six days later."</strong> Corrected. 1.82.6 is 22 March and 1.83.0 is 31 March, so nine days after the last clean release and seven after the poisoned ones.</li><li><strong>"PyPI quarantined them inside forty minutes."</strong> Narrowed. CloudSEK gives forty minutes as the period the packages were live and does not say who removed them. No source I could reach names the actor.</li><li><strong>"An unpinned Trivy scanner."</strong> Corrected. The vector was a leaked automation token and a non-atomic rotation, not an unpinned dependency.</li><li><strong>"2,488 corporate domains" from 433,909 files.</strong> Scoped. The domains were attributed from the 118,829 CI runner dumps, a subset of the archive.</li><li>Whether LiteLLM's maintainers disclosed the same day: I could not verify it, so it is not here.</li></ul>
<h2>Checked against</h2>
<ul><li><a href="https://www.cloudsek.com/blog/ai-supply-chain-breach-2500-companies-434000-cicd-pipelines">CloudSEK, &quot;2,500+ Companies and 434,000 CI/CD Pipelines Exposed in the Largest AI Supply Chain Breach of 2026&quot;, 11 August 2026</a></li><li><a href="https://www.helpnetsecurity.com/2026/08/13/litellm-breach-stolen-credentials-leak/">Help Net Security on Hudson Rock&#x27;s archive analysis, 13 August 2026</a></li><li><a href="https://pypi.org/pypi/litellm/json">PyPI JSON API for litellm, read 14 August 2026</a></li><li><a href="https://cveawg.mitre.org/api/cve/CVE-2026-33634">CVE-2026-33634, &quot;Trivy ecosystem supply chain briefly compromised&quot;, MITRE record</a></li><li><a href="https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json">CISA Known Exploited Vulnerabilities catalog, version 2026.08.14</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>What the encrypted reasoning paper actually counted</title>
      <link>https://imransiddique.com/notes/2026-08-13-encrypted-reasoning-denominators.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-13-encrypted-reasoning-denominators.html</guid>
      <pubDate>Thu, 13 Aug 2026 09:00:00 +0000</pubDate>
      <category>agent-security</category>
      <category>evidence</category>
      <category>retention</category>
      <description>Coverage of the reasoning-trace harvest reported three different totals as if they disagreed. They are all the paper&#x27;s own numbers, at three different denominators.</description>
      <content:encoded><![CDATA[<p><em>Coverage of the reasoning-trace harvest reported three different totals as if they disagreed. They are all the paper&#x27;s own numbers, at three different denominators.</em></p>
<p>A paper posted on 10 August showed that the encrypted reasoning blocks Anthropic, OpenAI and Google hand back to API clients were interchangeable inside each provider. A block issued in one session could be replayed into a different session, a different account, a different model in the same family. Hand it to a weaker sibling model and ask it to transcribe, and it reads the contents back in plain text.</p>
<p>No cipher was broken. The ciphertext was confidential and it was bound to nothing.</p>
<p>I want to record two things that the coverage got wrong, because both are the kind of error that survives into everyone's slide deck.</p>
<h2>The three totals are not a disagreement</h2>
<p>Different outlets reported the harvest as 182 credentials plus 367 pieces of personal information, or as 704 artifacts, or as 912. Read side by side those look like sources contradicting each other, and at least one write-up said so.</p>
<p>They are all in the paper, and they count different things:</p>
<ul><li><strong>912</strong> is every privacy artifact recovered, including the ones that came from benchmark sources rather than real users.</li><li><strong>704</strong> is the subset from genuine, non-benchmark user sessions. That is the number that means something, because those are real people's secrets.</li><li><strong>182 credentials and 367 pieces of personal information</strong> are two of the paper's three artifact categories, which is what its abstract leads with.</li></ul>
<p>Within the genuine-session set, the breakdown is 62 API keys, 33 passwords, 24 access tokens, 7 private keys and 30 personal email addresses. The scan covered 6,708 publicly posted agent trajectories and decoded 315,320 reasoning blocks.</p>
<p>The lesson is not about this paper. It is that coverage reliably repeats a number and drops the scope attached to it, and two correctly-reported numbers at different denominators then read as a contradiction. If you find yourself adjudicating between outlets, the answer is usually that nobody disagreed and the denominators were thrown away in the retelling.</p>
<h2>The dismissals were not a response to this paper</h2>
<p>Matthew Green reported the replay behaviour on 29 May, three months earlier. His account of what came back is worth quoting exactly, because a shortened version of it is now circulating with the causality reversed:</p>
<blockquote><p>OpenAI said my report was unreproducible</p></blockquote>
<blockquote><p>Anthropic quite reasonably told me they don't see any security implications in side channels or replays</p></blockquote>
<p>Several summaries have welded those quotes onto the August paper, producing a story where providers waved the researchers away and the team ran the harvest to force the issue. That story is not in the paper. Its disclosure section says all model providers acknowledged receipt of its report, and it attributes the earlier dismissals to Green's May disclosure by citation. Green himself calls Anthropic's answer a fine decision.</p>
<p>So the honest version is duller and more interesting: a cryptographer flagged the primitive in May, the providers judged it low severity, and a paper in August measured what the primitive was costing in the wild. Nobody in that sequence behaved badly. The gap was that no one had counted.</p>
<p>The paper's reproducibility statement notes that as of August the results no longer reproduce, because of mitigations the providers implemented following the disclosure. It does not date those mitigations, and I have not seen anyone who can.</p>
<h2>The part that is mine</h2>
<p>The missing control is a binding check at decrypt: this block was issued for this session, this account, this model, or it is refused. It runs at the provider's API boundary, because that is the only place the plaintext exists. No gateway, no agent framework and no policy layer can validate a binding on a payload it cannot open. That is enforcement, and it prevents the replay.</p>
<p>The evidence layer has a different job and it does not prevent anything. It decides what is allowed into the record in the first place.</p>
<p>Here is where that lands on my own work. The TRACE specification says, in its stated limitations:</p>
<blockquote><p>does not record the model's internal chain-of-thought, intermediate reasoning, or context window contents</p></blockquote>
<p>We file that under Known Limitations and it belongs there, because reasoning that changes behaviour without producing a tool call never reaches the record. It is also the reason a TRACE record contains nothing we cannot read back, which is the property this paper makes expensive to lack. Sixty-four of the recovered artifacts appeared only inside the opaque blocks and nowhere in the visible transcript. Some developers had cleaned the readable session before publishing it and shipped the secrets anyway.</p>
<p>A retention policy is a claim about fields you can read. If your trace carries a field you cannot open, you do not have a retention policy for it. You have a hope.</p>
<h2>Checked against</h2>
<ul><li><a href="https://arxiv.org/abs/2608.09867">Panfilov et al., &quot;Stealing Reasoning Traces from Proprietary LLM APIs&quot;, arXiv 2608.09867, 10 August 2026</a></li><li><a href="https://blog.cryptographyengineering.com/2026/05/29/fooling-around-with-encrypted-reasoning-blobs/">Matthew Green, &quot;Let&#x27;s talk about encrypted reasoning&quot;, 29 May 2026</a></li><li><a href="https://github.com/agentrust-io/trace-spec/blob/main/LIMITATIONS.md">TRACE specification, LIMITATIONS.md</a></li></ul>]]></content:encoded>
    </item>
    <item>
      <title>Two of the three endpoints checked authorization</title>
      <link>https://imransiddique.com/notes/2026-08-12-two-of-three-endpoints.html</link>
      <guid isPermaLink="true">https://imransiddique.com/notes/2026-08-12-two-of-three-endpoints.html</guid>
      <pubDate>Wed, 12 Aug 2026 09:00:00 +0000</pubDate>
      <category>agent-security</category>
      <category>api</category>
      <category>authorization</category>
      <description>The gym booking story is being told as an API with no authorization. The agent&#x27;s own account says two of the three operations it touched returned 403, and one did not.</description>
      <content:encoded><![CDATA[<p><em>The gym booking story is being told as an API with no authorization. The agent&#x27;s own account says two of the three operations it touched returned 403, and one did not.</em></p>
<p>An Australian man asked his personal assistant agent to book him a gym class. He was fourth on the waitlist. Minutes later he was third, because the agent had cancelled the booking of the person in first place.</p>
<p>The agent explained itself in plain text, and its wording matters:</p>
<blockquote><p>the API has zero authorisations checks on cancelling other people's reservations</p></blockquote>
<p>Nothing was jailbroken. There was no prompt injection. The agent read the API and used it exactly as documented.</p>
<h2>The detail that changes the story</h2>
<p>Most coverage stopped at "the API had no authorization". The agent's second message, which one outlet carried and the rest did not, says something more specific: <code>createReservation</code> and <code>joinWaitlist</code> both returned 403 Forbidden. Only <code>cancelReservation</code> was missing the ownership check.</p>
<p>So this was not a platform built without a security model. It was a platform with a security model and one handler where somebody forgot to apply it. Two of the three operations the agent touched behaved correctly.</p>
<p>That is a much more uncomfortable finding, because "we never did authorization" is a project you can schedule and "one of our handlers is missing a check" is a condition every codebase is in right now and cannot easily prove it is not.</p>
<p>I am scoping that carefully on purpose. Three operations were reported. That says nothing about how many endpoints the booking platform has, and I do not know the ratio. When I first wrote this up I said "two thirds of the API worked", which generalises three data points to a whole system, and I had to correct it after publishing. The honest statement is the narrow one.</p>
<h2>What kind of bug this is</h2>
<p>Broken Object Level Authorization: the caller is authenticated, the caller is permitted to invoke the operation, and nobody checks that the caller owns the specific object being operated on. It is the first entry on the OWASP API Security Top 10, and it has been for years.</p>
<p>The gap was presumably always there. Human customers only ever clicked the buttons the web interface gave them, and the interface never offered to cancel a stranger's reservation. The agent is not a new class of attacker. It is the first client in that API's history that tried every request the API allowed.</p>
<p>The missing control is per-request object authorization: does this caller own this object, not is this caller logged in. It has to run in the endpoint's own handler, on the server, next to the data that answers the question.</p>
<h2>Why my own layer would not have caught it</h2>
<p>AGT puts a deterministic policy engine between an agent and its tools, with no model anywhere in the safety path. On this incident it would have seen the agent issue a cancel call. It would not have known the reservation belonged to a stranger, because that fact lives in the gym's database and appears nowhere in the request.</p>
<p>A governance layer on the agent's side cannot make a check that only the server can make. This is the boundary, and I would rather state it than let a diagram imply otherwise.</p>
<p>There is a second thing worth separating, because I blurred it in the first version of this and it is worth not repeating. Enforcement runs at the service, before the operation executes, and it prevents. Evidence is written after the fact, it is tamper-evident, and it proves. A log would not have saved the person whose class was cancelled. It would only have made the conversation afterwards short.</p>
<p>OpenClaw was at 385,960 stars when I read the repository. Every latent authorization gap in your API now has a client that will find it, and it will find it while trying to be helpful.</p>
<h2>What did not survive checking</h2>
<ul><li>One outlet placed the man in Melbourne. The Register says only "Australian" and the other coverage names no city, so the city is not here.</li><li>Reports of how far ahead the agent could book range from "several weeks" to "months", so no number is here either.</li><li>Several outlets called this Australia's first autonomous cyberattack. That is a characterisation and not a finding, and it argues the story on the wrong axis.</li><li>The original ABC News report is the primary source. abc.net.au blocks the crawler I use, so I have not read it directly. Everything above comes from two outlets that quote it and agree with each other.</li></ul>
<h2>Checked against</h2>
<ul><li><a href="https://thenextweb.com/">The Next Web, coverage carrying the agent&#x27;s second message</a></li><li><a href="https://www.theregister.com/">The Register, coverage quoting ABC News (Australia), 10 August 2026</a></li><li><a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/">OWASP API Security Top 10, API1 Broken Object Level Authorization</a></li><li><a href="https://api.github.com/repos/openclaw/openclaw">openclaw/openclaw star count read at api.github.com, 12 August 2026</a></li></ul>]]></content:encoded>
    </item>
  </channel>
</rss>
