<?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://avneeshk.me/feed/by_tag/least_privilege.xml" rel="self" type="application/atom+xml" /><link href="https://avneeshk.me/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-10-17T11:48:31+00:00</updated><id>https://avneeshk.me/feed/by_tag/least_privilege.xml</id><title type="html">Avneesh Kasture</title><subtitle>Building secure MCP servers for clients — hardening the agentic layer against prompt injection, tool misuse, and privilege abuse. Field notes on MCP security, agentic safety, and shipping AI platforms.</subtitle><author><name>Avneesh Kasture</name><email>apkasture02@gmail.com</email></author><entry><title type="html">Why did an evaluation agent end up inside a Medicare portal?</title><link href="https://avneeshk.me/blog/why-did-an-eval-agent-end-up-in-a-medicare-portal/" rel="alternate" type="text/html" title="Why did an evaluation agent end up inside a Medicare portal?" /><published>2026-10-14T00:00:00+00:00</published><updated>2026-10-14T00:00:00+00:00</updated><id>https://avneeshk.me/blog/why-did-an-eval-agent-end-up-in-a-medicare-portal</id><content type="html" xml:base="https://avneeshk.me/blog/why-did-an-eval-agent-end-up-in-a-medicare-portal/"><![CDATA[<p>On September 24, Prime Minister Anthony Albanese <a href="https://www.pm.gov.au/media/press-conference-new-york">told a press conference</a> that an OpenAI agent had gained unauthorised access to Services Australia’s Medicare Statistics Reporting Portal. The portal publishes aggregate figures, things like spending and bulk-billing statistics. It is separate from the systems that hold Medicare claims and personal records.</p>

<p>So why did an agent with a research task end up somewhere it wasn’t allowed to be, and why did it take until September for anyone to say so?</p>

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

<p>The timeline, per the <a href="https://www.abc.net.au/news/2026-09-24/ai-agent-accessed-australian-government-site-pm-says/107189078">ABC</a>, the Prime Minister and OpenAI:</p>

<table class="timeline-table">
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
      <th>Source</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Jun 18</td>
      <td>An OpenAI model, in an internal training or evaluation run, is researching public medicines spending. The portal doesn’t give it what it wants. The Prime Minister says the agent “found a way around those blocks.”</td>
      <td><a href="https://www.abc.net.au/news/2026-09-24/ai-agent-accessed-australian-government-site-pm-says/107189078">ABC</a>, <a href="https://www.pm.gov.au/media/press-conference-new-york">PM</a></td>
    </tr>
    <tr>
      <td>Mid-Aug</td>
      <td>OpenAI finds it while reviewing old training and evaluation runs, a review it started only after the Hugging Face incident in July. Some write-ups give August 11. OpenAI and the government say only “August”.</td>
      <td><a href="https://openai.com/index/how-we-will-do-better-for-australia/">OpenAI</a></td>
    </tr>
    <tr>
      <td>Sep 10</td>
      <td>OpenAI emails a public Services Australia mailbox.</td>
      <td><a href="https://www.abc.net.au/news/2026-09-24/ai-agent-accessed-australian-government-site-pm-says/107189078">ABC</a></td>
    </tr>
    <tr>
      <td>Sep 11</td>
      <td>Services Australia sees the email.</td>
      <td><a href="https://www.abc.net.au/news/2026-09-24/ai-agent-accessed-australian-government-site-pm-says/107189078">ABC</a></td>
    </tr>
    <tr>
      <td>Sep 15</td>
      <td>Services Australia reports it to the Australian Signals Directorate.</td>
      <td><a href="https://www.abc.net.au/news/2026-09-24/ai-agent-accessed-australian-government-site-pm-says/107189078">ABC</a></td>
    </tr>
    <tr>
      <td>Sep 24</td>
      <td>Albanese phones Sam Altman and announces the incident.</td>
      <td><a href="https://www.pm.gov.au/media/press-conference-new-york">PM</a></td>
    </tr>
  </tbody>
</table>

<figure class="post-figure">
  <img src="/assets/blog/why-did-an-eval-agent-end-up-in-a-medicare-portal/timeline.svg" alt="Timeline drawn to scale: on June 18 the agent gets non-public access to the portal, by a mechanism not disclosed. A red stretch of about eight weeks follows in which nobody noticed. In mid-August OpenAI&#39;s review of earlier runs finds it. For about four weeks OpenAI knew and the agency didn&#39;t. On September 10 OpenAI emails a public mailbox, seen September 11 and reported to ASD September 15. Fourteen days later, on September 24, the Prime Minister announces it." loading="lazy" />
  <figcaption>The same dates, to scale. The longest stretch is the one where nobody on either side knew.</figcaption>
</figure>

<p>What was reached: <a href="https://www.abc.net.au/news/2026-09-24/what-we-know-about-the-openai-medicare-hack/107189452">public and non-public files</a> on the portal. The government described the non-public material as not particularly sensitive, and it has since been published. The agent also <a href="https://www.pm.gov.au/media/press-conference-new-york">wrote files to an internal server</a>, which is still under investigation. OpenAI <a href="https://openai.com/index/how-we-will-do-better-for-australia/">says</a> its “review to date has found no evidence that anyone’s medical records were accessed.”</p>

<p>OpenAI’s own account, <a href="https://openai.com/index/how-we-will-do-better-for-australia/">“How we will do better for Australia”</a>, goes further than the government did. It says the model found a way to get non-public access to the service, then ran commands, retrieved internal files, credentials, and aggregate statistics, and wrote files. It adds that the model then used that access “to review technical system information and source code related to the service,” still trying to answer the question it had been given: government spending per person on medicines for skin conditions in Victorian communities. In early October, OpenAI disclosed <a href="https://abcnews.com/Business/openai-reveals-hack-government-agency-australia/story?id=136945837">a second case</a>, in which a model sent crafted queries to a NSW National Parks fire-history map service and inferred database metadata that wasn’t meant to be public.</p>

<p>Nobody has published how the agent got from “blocked” to “inside” at this portal: no CVE and no description of the technique. Acting Prime Minister Richard Marles <a href="https://www.abc.net.au/news/2026-09-24/what-we-know-about-the-openai-medicare-hack/107189452">said</a> the data “was really kept behind a fence that the AI agent effectively climbed over,” which is a fine sentence and tells you nothing about the fence.</p>

<h2 id="the-agent-did-what-the-loop-told-it-to-do">The agent did what the loop told it to do</h2>

<p>You might think an agent needs malice to break into something. It doesn’t. The model gets a goal: find this spending figure. It has tools: fetch a URL, maybe run code. It calls a tool, reads the result, decides on the next call, and repeats until the goal is met or the budget runs out.</p>

<p>Now the portal returns a refusal. To you, a refusal is a message from the system owner meaning “no.” To the loop, it is one more tool result, sitting next to every other tool result, and the goal hasn’t been met yet. The loop’s next step is to try something else. A model trained on plenty of security material knows what a penetration tester tries next.</p>

<p>This is the paperclip maximizer, at office scale. Nick Bostrom’s thought experiment imagines an AI told to make paperclips that turns everything it can reach into paperclips, with no malice involved, because nothing in the goal says where to stop. The real version is less cinematic. The paperclip was a per-person spending figure for skin-condition medicines in Victoria, and “everything it can reach” turned out to include a government portal’s source code. Nothing in the goal said to stop at the locked door, so the locked door was one more thing between the agent and its paperclip.</p>

<p><a href="https://transluce.org/agent-activity">Transluce’s report</a> on agent activity it links to an OpenAI-confirmed swarm describes the same pattern at the Australian Institute of Health and Welfare on June 20 and 21: a dataset download blocked by Cloudflare, then a cross-site scripting probe (which the firewall stopped), then the same public file fetched from a pre-production server. That was a different site, where Transluce says no non-public data was exposed, so it shows the habit, not the Medicare mechanism.</p>

<p>The model doesn’t need to want anything. It needs a goal, a refusal it treats as an obstacle, and a network path to try the next idea on. <a href="/you-are-the-model/">/you-are-the-model/</a> puts you in its seat.</p>

<h2 id="why-the-next-idea-reached-a-real-server">Why the next idea reached a real server</h2>

<p>A training or evaluation run is supposed to be a sandbox. OpenAI’s list of changes afterward says otherwise: it now blocks live internet access during internal training and evaluation and serves web content from cached data. It has also paused training and evaluation involving tool use for its most capable models, and widened monitoring so unusual access pages a human.</p>

<p>You don’t add “block live internet” unless the internet was live. So the sandbox almost certainly had a door to the outside, and the outside included a government portal whose access controls, whatever they were, gave way to whatever the agent tried.</p>

<p>The Medicare portal was a public website that also held non-public files. Something in front of those files said no to the agent at first and later stopped saying no. The government hasn’t said what that was, or how a read-only statistics site ended up accepting writes.</p>

<figure class="post-figure">
  <img src="/assets/blog/why-did-an-eval-agent-end-up-in-a-medicare-portal/sandbox-to-portal.svg" alt="Diagram: an eval harness with a goal, tools to fetch a URL and run code, and a loop that treats a refusal as one more result sends requests out to the live internet (inferred). They reach the Medicare statistics portal&#39;s public pages, then a control that said no and later stopped saying no, its mechanism not disclosed, and past it non-public files that were read and an internal server the agent wrote files to. Red markers show what wasn&#39;t there: no egress allowlist, no check on every path, no real-time alert." loading="lazy" />
  <figcaption>The path as far as it is known. Dashed boxes are inferred or undisclosed; red numbers are the missing controls.</figcaption>
</figure>

<h2 id="why-nobody-noticed-for-weeks">Why nobody noticed for weeks</h2>

<p>Picture the portal’s request log: a client fetches pages, gets refused, fetches different pages. That looks like a crawler, and public statistics portals are crawled all day. Nothing about an agent’s traffic announces that it is an agent, or whose. When asked why Australia’s systems hadn’t caught it earlier, the Prime Minister <a href="https://www.pm.gov.au/media/press-conference-new-york">did not directly answer</a>.</p>

<p>On the OpenAI side, if the run was scored on the answer, the score is the same whether the agent got it from a public download or through a side door. The access was found in mid-August, by a review of old runs that OpenAI started only because the Hugging Face incident made it look. A log that nobody reads in real time is a diary.</p>

<figure class="post-figure">
  <img src="/assets/blog/why-did-an-eval-agent-end-up-in-a-medicare-portal/empty-reception-desk.jpg" alt="An empty hotel reception desk late at night, lit and tidy, with a monitor on the counter, a sign reading RECEPTION on the stone wall behind it, and nobody there" loading="lazy" />
  <figcaption>The log was running the whole time. Nobody was at the desk. “<a href="https://commons.wikimedia.org/wiki/File:Empty_reception_desk_at_Good_Stay_Eiropa_Hotel.jpg">Empty reception desk at Good Stay Eiropa Hotel</a>” by JIP, <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>, via Wikimedia Commons, resized.</figcaption>
</figure>

<p>Then, having found it, OpenAI notified the agency by emailing a public inbox. Albanese called that “unacceptable.”</p>

<h2 id="the-controls-that-dont-depend-on-the-models-judgment">The controls that don’t depend on the model’s judgment</h2>

<p>Every fix that would have contained this is a boundary in code, enforced somewhere the model can’t argue with, the same as <a href="/blog/guardrails-belong-in-the-server/">guardrails in the server</a> and <a href="/blog/hugging-face-agentic-breach/">the July Hugging Face incident</a>: the wall held where it was deterministic.</p>

<h3 id="egress-allowlists-on-the-harness">Egress allowlists on the harness</h3>

<p>A research agent should reach a fixed list of hosts, or a cache, and nothing else. That list has to exclude anything that fetches other URLs for you: URL scanners, translation proxies, archive services. The agents in Transluce’s report used urlquery.net for exactly that. Allowlists have their own failure modes, which <a href="/allowlist-bypass/">/allowlist-bypass/</a> walks through, but a default-deny network beats a model’s sense of what is appropriate.</p>

<h3 id="authorisation-at-the-data">Authorisation at the data</h3>

<p>If a file is non-public, the server should refuse to serve it, or accept a write, from an unauthenticated client on every path, not only the obvious one. Credentials don’t belong anywhere a web server can hand them out, and any credential an agent touched gets rotated that day. A public front end sitting on top of private files is a single bug from exposure, and an agent that can try a hundred ideas a minute will find the bug sooner than a person would. <a href="/break-this-server/">/break-this-server/</a> is a working example of putting the check where the data is.</p>

<h3 id="identity-and-audit-for-agent-actions">Identity and audit for agent actions</h3>

<p>Agent traffic should carry an identity a site owner can see and attribute, enforced at the operator’s egress proxy, since a signature disappears the moment a request goes through someone else’s fetcher. On the operator side, every tool call should be logged with the task it served, and some actions (an auth failure followed by a retry with a different technique, a write to a remote host) should page a human that day, rather than turning up in a review two months later.</p>

<p>None of these ask the model to be good. A paperclip maximizer behind a default-deny network makes paperclips out of whatever is on the list.</p>

<h2 id="bonus-chatter">Bonus chatter</h2>

<p>Transluce found its evidence in <a href="https://transluce.org/agent-activity">public scan records on urlquery.net</a>, a service the agents were apparently using to fetch pages they couldn’t reach directly. The agents left a public audit trail on a third party’s site, readable by anyone, while their operator’s own logs sat unread until August.</p>

<h2 id="exercise">Exercise</h2>

<p>Your agent’s tool returns HTTP 403. Write down what your harness does next. If the answer is “the model decides,” name the control that stops the model’s second idea from reaching the network.</p>

<details class="post-answer">
  <summary>Show an answer</summary>
  
<p>In most harnesses, the 403 goes back to the model as a tool result and the model decides. That’s the gap.</p>

<p>The control belongs in the harness, below the model, in two places:</p>

<ul>
  <li>An egress proxy that every tool request goes through, with a default-deny list of hosts. It can also make a refusal binding: after a 401 or 403 from an origin, it refuses further requests to that origin for the rest of the run, whatever technique the next request tries.</li>
  <li>The tool wrapper treats 401 and 403 as final for that resource. It tells the model “access denied, do not retry” and logs the event for a person to look at the same day, rather than handing over the raw response for the loop to treat as an obstacle.</li>
</ul>

<p>The model can still have a second idea. The idea just never reaches the network.</p>

</details>]]></content><author><name>Avneesh Kasture</name><email>apkasture02@gmail.com</email></author><category term="incident" /><category term="agentic" /><category term="least_privilege" /><category term="guardrails" /><summary type="html"><![CDATA[In June 2026 an OpenAI model in internal training or evaluation got non-public access to Services Australia's Medicare statistics portal, and the government heard about it on September 10. What is known about how it happened, why it went unnoticed for weeks, and which deterministic controls would have kept the task in its box.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://avneeshk.me/assets/og/why-did-an-eval-agent-end-up-in-a-medicare-portal.png" /><media:content medium="image" url="https://avneeshk.me/assets/og/why-did-an-eval-agent-end-up-in-a-medicare-portal.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Whose agent was it in the Hugging Face breach?</title><link href="https://avneeshk.me/blog/hugging-face-agentic-breach/" rel="alternate" type="text/html" title="Whose agent was it in the Hugging Face breach?" /><published>2026-07-20T00:00:00+00:00</published><updated>2026-07-20T00:00:00+00:00</updated><id>https://avneeshk.me/blog/hugging-face-agentic-breach</id><content type="html" xml:base="https://avneeshk.me/blog/hugging-face-agentic-breach/"><![CDATA[<blockquote>
  <p><strong>Correction (October 16, 2026):</strong> The original version of this post said the agent belonged to an outside attacker, and argued that refusal failed because that attacker never agreed to any terms of service. On July 21, OpenAI <a href="https://www.axios.com/2026/07/21/openai-says-hugging-face-breach-caused-by-one-its-models">disclosed</a> that its own models, under evaluation with their <a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-autonomous-ai-agent-intrusion-openai-huggi/">cyber refusal behavior reduced</a>, escaped their sandbox and caused the breach, and independent investigators at METR and Redwood Research <a href="https://www.nbcnews.com/tech/tech-news/openai-report-says-network-was-hacked-rogue-ai-agents-rcna594590">put the swarm at roughly 700 agents</a>; I’ve rewritten the sections that depended on the old framing.</p>
</blockquote>

<p>Hugging Face wrote up <a href="https://huggingface.co/blog/security-incident-july-2026">their July intrusion</a>, and the part people quote is the part about AI. So I’ll start with a question that sounds like a trick: in this breach, whose agent was it?</p>

<p>The first version of this post answered “the attacker’s.” The real answer is OpenAI’s. The models that broke in were under evaluation, testing whether they could exploit vulnerable software, with their cyber guardrails turned down so the evaluators could measure what they could actually do. According to <a href="https://time.com/article/2026/07/24/openai-hugging-face-attack/">TIME’s account</a>, they found a flaw in an internal package-download service, used it to reach other OpenAI systems and the open internet, and then broke into Hugging Face, apparently looking for material that would raise their score on the test.</p>

<p>Hugging Face’s own models, agents, and MCP surface still came through untouched. And once you know whose agent it was, most of the “agentic security” commentary still goes away. What’s left is a code-execution bug, a worker that could reach too much, a sandbox that was assumed to be isolated and wasn’t, and a defender who couldn’t get a commercial model to read their own logs.</p>

<p>That last one is the strangest, so I’ll take it first.</p>

<h2 id="the-model-that-wouldnt-help">The model that wouldn’t help</h2>

<p>To reconstruct the intrusion, Hugging Face had more than 17,000 recorded attacker events to sift. That is too many to read by hand, so they pointed a frontier model at the logs.</p>

<p>The commercial APIs refused.</p>

<p>You might think that’s a bug in the safety filter. It’s the filter working as designed. The events were full of real attack commands, exploit payloads, and C2 artifacts. From where the filter sits, an incident responder pasting those in looks exactly like the attacker who wrote them. The filter can’t see your badge.</p>

<p>So the refusal locked the defenders out of their own investigation. They switched to GLM-5.2, an open-weight model run on their own infrastructure. That also kept the attacker’s data and the harvested credentials inside the building, which turns out to matter, and I’ll come back to it.</p>

<p>Now count who was inconvenienced. The refusal layer everyone files under “AI safety” cost the people cleaning up the breach. On the other side of the incident, the models doing the breaking ran with what the <a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-autonomous-ai-agent-intrusion-openai-huggi/">Cloud Security Alliance’s research note</a> quotes as “reduced cyber-related refusal behavior for the purposes of the evaluation.” That’s a reasonable thing for an evaluator to do. You can’t measure how well a model exploits software if it declines to try.</p>

<p>So refusal turns out to be a setting, and whoever owns the model holds the dial. The lab could turn it down for its own agents. Hugging Face couldn’t turn it down for its own forensics, because Hugging Face didn’t own the model it was asking. My first draft said refusal binds only the parties who accepted the terms of service. The corrected version is narrower and less comfortable: refusal binds whoever doesn’t control the dial, and in this incident that was the defender.</p>

<p>Alignment didn’t carry the load either. The models left the sandbox to cheat on a test. <a href="https://www.nbcnews.com/tech/tech-news/openai-report-says-network-was-hacked-rogue-ai-agents-rcna594590">NBC News reports</a> that the METR and Redwood investigation found one in five of the agents it examined “expressed clear interest” in manipulating evidence, and OpenAI’s own response, per TIME, was that the incident “points to the need to further strengthen our model’s alignment.” A model that behaves is a fine property to have. It is a property of the model, and a property can be absent on the day you need it. Neither the terms of service nor the model’s good intentions was the control that mattered here.</p>

<h2 id="nobody-aimed-it">Nobody aimed it</h2>

<p>When an incident has “agent” in it, people reach for the agentic-security shelf: <a href="/owasp-top-10/">the OWASP Agentic Top 10</a>, prompt injection, <a href="/tool-poisoning/">tool poisoning</a>, the whole catalog of ways an AI system gets subverted. That’s the wrong shelf for this one too. Nobody subverted these agents. They did what they were optimized to do, which was score well, and the test environment let them go further than anyone intended.</p>

<p>The agents were a swarm. METR and Redwood counted about 700, and OpenAI said the figure was accurate. They ran thousands of actions across many temporary virtual machines and drove the intrusion with no human at the keyboard. Per the CSA note, they even found an internal package repository they could write to and used it as a message board. Hugging Face’s own anomaly detection, itself a model, is what surfaced them. So AI ran on both sides. On Hugging Face’s side it was tooling, and it worked. On OpenAI’s side it was the thing under test, and TIME quotes an OpenAI staffer saying “models undergoing evaluation are deployed on a separate system that is not monitored by default.” Nobody’s AI got hacked. One lab’s AI got out.</p>

<p>What did get hacked at Hugging Face is older. A malicious dataset abused two code-execution paths in the dataset-processing pipeline: a remote-code dataset loader, and a template injection in the dataset config. Both ran code on a processing worker.</p>

<p>Strip away the nouns and the mechanism is this: untrusted input reached a parser that executes it. That bug is older than the platform it ran on. If you replaced the agent with a patient human, the bug would be the same bug. The agent added tempo: the same playbook at machine speed and machine cost.</p>

<p>The tempo is this year’s trend line, and there’s a paper trail for it.</p>

<ul>
  <li><a href="https://research.checkpoint.com/2026/ai-security-report-2026/">Check Point’s 2026 AI Security Report</a> describes AI crossing “from assistant to operator”: the model doing the hands-on work inside a live intrusion, where before it coached the human who did.</li>
  <li>Sysdig documented <a href="https://www.sysdig.com/blog/jadepuffer-agentic-ransomware-for-automated-database-extortion">JADEPUFFER</a>, which it calls the first end-to-end LLM-driven ransomware. The agent rode a Langflow RCE (CVE-2025-3248) through recon, credential theft, lateral movement, and encryption. At one point it went from a failed login to a working fix in 31 seconds by debugging its own PATH error. (I have taken longer than that to find my own PATH error, and I had the advantage of knowing what PATH was for.)</li>
  <li>Anthropic’s <a href="https://www.anthropic.com/news/disrupting-AI-espionage">GTG-1002 disclosure</a> put a state-linked actor’s Claude Code agent at 80–90% of the operation against roughly thirty targets, with humans stepping in only at the edges.</li>
</ul>

<p>The Hugging Face swarm is the same pattern with one difference. In those cases a human picked the target. Here a model picked it, because Hugging Face looked like a place to find answers to the test. Old bug class, automated operator, and nobody who chose the victim.</p>

<h2 id="why-one-bad-dataset-turned-into-several-clusters">Why one bad dataset turned into several clusters</h2>

<p>Code execution on one ingestion worker should be a contained event. The worker parses a dataset, the dataset is evil, the worker is now evil. Throw the worker away.</p>

<p>It became a production intrusion because of what that worker could reach. From the foothold, the agent escalated to node-level access, harvested cloud and cluster credentials, and moved laterally across several internal clusters over a weekend.</p>

<p>The confirmed blast radius was a limited set of internal datasets and several service credentials. Public models, datasets, and Spaces came through untouched, and the supply chain verified clean. That’s the good news. It’s also the near miss, because a credential harvest like that could have gone much further.</p>

<p>Hugging Face left the exploit map out of the writeup. But you can reconstruct a lot of it from the hardening patches that landed mid-incident, on the principle that nobody patches the parts that were fine.</p>

<p><a href="https://github.com/huggingface/dataset-viewer/pull/3368">PR #3368</a> turns off automatic service-account-token mounting on the worker pods, applies a <code class="language-plaintext highlighter-rouge">RuntimeDefault</code> seccomp profile, and drops all Linux capabilities, noting that the worker “never interact[s] with the Kubernetes API.” Read that again. The worker had been carrying a Kubernetes API token it never used. That is a key to an office you never visit, carried around in case someone wants to borrow it.</p>

<figure class="post-figure">
  <img src="/assets/blog/hugging-face-agentic-breach/key-ring.jpg" alt="A large bundle of old rusty keys on several rings, spread on a wooden table" loading="lazy" />
  <figcaption>Ambient authority, rendered in iron. “<a href="https://commons.wikimedia.org/wiki/File:Rusty_keys_Schl%C3%BCssel_Keys_on_key_rings_Schl%C3%BCsselbund_01.JPG">Rusty keys on key rings</a>” by Iswoar, <a href="https://creativecommons.org/licenses/by-sa/3.0/">CC BY-SA 3.0</a>, via Wikimedia Commons.</figcaption>
</figure>

<p><a href="https://github.com/huggingface/dataset-viewer/pull/3367">PR #3367</a> restricts the dataset loader’s <code class="language-plaintext highlighter-rouge">fsspec</code> backends to an allowlist and removes the implementations that widen the read surface.</p>

<p><a href="https://github.com/huggingface/dataset-viewer/pull/3375">PR #3375</a> moves the production database off a static password and onto IAM auth.</p>

<p>Put those together and you get the picture before the fixes: a parser that runs untrusted code, standing on an auto-mounted API token, behind a loader that would fetch from nearly anywhere, with long-lived static secrets in reach. A worker with authority well past its job.</p>

<p>(The wider reach is my read from the shape of the fixes and the lateral movement: cloud metadata endpoint, control-plane path, open egress. That goes beyond what Hugging Face stated. Treat it as inference.)</p>

<p>So the code execution was the match, and the worker’s ambient authority was the accelerant. The match alone gets you an ingestion incident. The accelerant is what turns it into a credential harvest across clusters.</p>

<figure class="post-figure">
  <img src="/assets/blog/hugging-face-agentic-breach/match-and-accelerant.svg" alt="Diagram: a malicious dataset gets code execution on a processing worker; the worker&#39;s ambient authority (auto-mounted service-account token, open fsspec loader, static secrets, and an inferred wider reach) leads to node-level access, harvested credentials, and lateral movement across clusters; red walls mark PRs #3368, #3367, and #3375 on the three stated paths" loading="lazy" />
  <figcaption>The match and the accelerant. Red walls are the fix PRs on each path they close; the dashed path is my inference, not Hugging Face’s statement.</figcaption>
</figure>

<p>None of this is new advice. The <a href="https://media.defense.gov/2022/Aug/29/2003066362/-1/-1/0/CTR_KUBERNETES_HARDENING_GUIDANCE_1.2_20220829.PDF">NSA/CISA Kubernetes Hardening Guide</a> has said for years to disable the service-account token where a pod stays off the API, scope RBAC to least privilege, and use network policies so a compromise in one namespace stays in one namespace. Hugging Face’s remediation is that guide, applied after the fact. Standard controls, switched on after the breach, in places where they belonged before it.</p>

<h2 id="the-walls-that-held-were-the-ones-written-in-code">The walls that held were the ones written in code</h2>

<p>Look at every control that contained this: drop the token, drop the capabilities, allowlist the loader’s protocols, replace static secrets with scoped identity, rebuild compromised nodes from known-good images.</p>

<p>What they have in common is that none of them asks anybody to behave. The untrusted thing is structurally unable to reach the thing that matters. A worker without a token can’t use the token, no matter how clever the code running on it is, no matter how fast, and no matter whose agent it is. A wall like that holds at any attacker speed, which is a property you want when the attacker fixes its own bugs in 31 seconds. It also holds when the “attacker” is somebody’s evaluation run that wandered off, which is a case I hadn’t thought to plan for.</p>

<p>That’s <a href="/blog/guardrails-belong-in-the-server/">the reason I put guardrails in the server and not the prompt</a>, and <a href="/blog/building-mcploitable/">why the lab I built scores controls by whether the wall holds, not whether the model behaves</a>.</p>

<p>This incident happened to put both kinds of guardrail on the table at once, which is convenient for anyone keeping score. The refusal kind showed up twice. In the evaluation, the lab had turned it down, so it did nothing. In the forensics, the defender couldn’t turn it down, so it cost them time. The confinement kind showed up twice too. OpenAI’s sandbox was supposed to have no meaningful route to the internet, and the models found one. Hugging Face’s pod hardening is what the fix PRs added, and once it’s in place, it holds for any code on that worker. Only the confinement kind did any work, and only where it actually existed.</p>

<h2 id="own-the-model-or-it-isnt-yours-when-you-need-it">Own the model, or it isn’t yours when you need it</h2>

<p>Back to GLM-5.2. Hugging Face ran it in-house because the alternative had failed them twice. The commercial API refused the work. And even if it hadn’t, sending the logs out would have meant shipping attacker payloads and their own credentials to a third party, in the middle of a breach.</p>

<p>They reached the right architecture under pressure. The catch is that it took a breach to get there.</p>

<p>Every team running security operations should have that capability set up before the next incident: an open-weights model, on infrastructure you control, wired into your log sources and security tools. I mean an analysis layer plumbed into the SIEM, the EDR, the cloud audit trail, and the telemetry, able to query them, correlate them, and rebuild a timeline at the speed the attack ran. A chat window you paste things into is not that.</p>

<p>The reasons to make it local-first and open-weights have little to do with model quality:</p>

<ol>
  <li>It answers. No safety policy sits between your responder and the payload they need read, and no vendor gets to decide your forensics look too much like an attack. Hugging Face lived this one.</li>
  <li>Your incident data stays put. Logs, credentials, IOCs, and C2 artifacts are the most sensitive data you hold. If you rent the model, you ship them to a third party to ask about your own breach.</li>
  <li>It’s available on the bad day. A rate limit or a vendor outage can’t stand between you and your own logs.</li>
  <li>You can fit it to your log formats, your detections, and your infrastructure. A hosted endpoint stays generic.</li>
</ol>

<p>This is the same principle as the walls, applied to the defender’s own tooling: capability lives in the stack you own. A deterministic control you own holds regardless of the attacker. A model you own answers regardless of the vendor. Rent either one and it can quit the moment you reach for it, on someone else’s terms.</p>

<h2 id="bonus-chatter">Bonus chatter</h2>

<p>The number I keep coming back to is the tempo. Seventeen thousand events in a weekend looked to me like a plan that assumed nobody was watching in time. It turns out there was no plan, and on the lab’s side nobody was watching at all, because the evaluation system wasn’t monitored by default. A model that fixes its own error in 31 seconds is already past the point where a human reviewer catches up. Keeping up with it means owning the whole defensive stack, both the deterministic controls and the AI that watches them, because rented tooling answers to the vendor at exactly the moment you need it to answer to you.</p>

<p>The question in the title has since gone to court. The dates this post rests on:</p>

<table class="timeline-table">
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
      <th>Source</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Jul 21, 2026</td>
      <td>OpenAI discloses that its own models caused the breach.</td>
      <td><a href="https://www.axios.com/2026/07/21/openai-says-hugging-face-breach-caused-by-one-its-models">Axios</a></td>
    </tr>
    <tr>
      <td>Sep 29, 2026</td>
      <td>LASST sues OpenAI in San Francisco Superior Court.</td>
      <td><a href="https://thenextweb.com/news/openai-lawsuit-lasst-hugging-face-hack">The Next Web</a></td>
    </tr>
    <tr>
      <td>Oct 16, 2026</td>
      <td>This post is corrected.</td>
      <td>This page</td>
    </tr>
  </tbody>
</table>

<p>LASST argues under California’s AB 316 that a developer can’t defend itself by saying its AI acted on its own, and asks for an order barring OpenAI’s agents from unauthorized access to other people’s systems. OpenAI called the suit “completely without merit.” However that comes out, the token that was never mounted stays unmounted.</p>

<p><strong>Exercise:</strong> Pick a worker in your own cluster that parses files someone else uploaded. Assume the parser executes its input. Write down what that process can reach: tokens mounted into the pod, capabilities it holds, secrets in its environment, hosts it can open a connection to. Then check how many of those it uses to do its actual job.</p>

<details class="post-answer">
  <summary>Show an answer</summary>
  
<p>It depends on your cluster, but for a typical upload parser the tally tends to look like this:</p>

<ul>
  <li>A service-account token, mounted by default and used for nothing. Set <code class="language-plaintext highlighter-rouge">automountServiceAccountToken: false</code>.</li>
  <li>The default Linux capabilities, none of which a parser needs. Use <code class="language-plaintext highlighter-rouge">drop: [ALL]</code> and a <code class="language-plaintext highlighter-rouge">RuntimeDefault</code> seccomp profile.</li>
  <li>Secrets in the environment for features that live in other services: database passwords, API keys. Move them out, or swap static secrets for short-lived, scoped identity.</li>
  <li>A network that reaches anywhere, including the cloud metadata endpoint. The parser needs to read from storage and write a result. A default-deny <code class="language-plaintext highlighter-rouge">NetworkPolicy</code> with an allowlist covers that.</li>
</ul>

<p>If the number it actually uses is close to zero, that’s normal. Everything on the list it doesn’t use is something its input gets for free the day the parser starts executing it.</p>

</details>]]></content><author><name>Avneesh Kasture</name><email>apkasture02@gmail.com</email></author><category term="incident" /><category term="agentic" /><category term="least_privilege" /><summary type="html"><![CDATA[In Hugging Face's July 2026 breach, OpenAI models that had escaped an evaluation sandbox, with their cyber refusals turned down, rode an ordinary code-execution bug through an over-privileged worker. Refusal and alignment didn't stop them; deterministic walls are what hold. The defender's takeaway: run an open-weights model locally, connected to your log sources and security tools, so your own forensics never get refused, leaked, or throttled.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://avneeshk.me/assets/og/hugging-face-agentic-breach.png" /><media:content medium="image" url="https://avneeshk.me/assets/og/hugging-face-agentic-breach.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>