<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Writing - Ashwin Gopalsamy</title>
    <link>https://ashwingopalsamy.in/writing/tags/security/</link>
    <description>Staff Software Engineer scaling authorization infrastructure at Pismo, Visa.</description>
    <language>en-us</language>
    <lastBuildDate>Wed, 25 Mar 2026 00:00:00 &#43;0000</lastBuildDate>
    
    <atom:link href="https://ashwingopalsamy.in/writing/tags/security/feed.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Anatomy of a Supply Chain Attack: LiteLLM on PyPI</title>
      <link>https://ashwingopalsamy.in/writing/anatomy-of-a-supply-chain-attack-litellm-on-pypi/</link>
      <pubDate>Wed, 25 Mar 2026 00:00:00 &#43;0000</pubDate>
      <guid isPermaLink="true">https://ashwingopalsamy.in/writing/anatomy-of-a-supply-chain-attack-litellm-on-pypi/</guid>
      <description>&lt;p&gt;On March 24, 2026, Callum McMahon at &lt;a href=&#34;https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;FutureSearch&lt;/a&gt;
 was testing a &lt;a href=&#34;https://futuresearch.ai/blog/no-prompt-injection-required/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Cursor MCP plugin&lt;/a&gt;
 that pulled in &lt;a href=&#34;https://pypi.org/project/litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;litellm&lt;/a&gt;
 as a transitive dependency. He never ran &lt;code&gt;pip install litellm&lt;/code&gt; himself. The plugin resolved it automatically.&lt;/p&gt;
&lt;p&gt;Shortly after, his machine became unresponsive. RAM exhausted. He traced it to a newly installed litellm package, decoded an obfuscated payload hidden inside it, and &lt;a href=&#34;https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;published the first disclosure&lt;/a&gt;
. His team used &lt;a href=&#34;https://claude.ai/claude-code&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Claude Code&lt;/a&gt;
 to help root-cause the crash.&lt;/p&gt;</description>
      <content:encoded>&lt;p&gt;On March 24, 2026, Callum McMahon at &lt;a href=&#34;https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;FutureSearch&lt;/a&gt;
 was testing a &lt;a href=&#34;https://futuresearch.ai/blog/no-prompt-injection-required/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Cursor MCP plugin&lt;/a&gt;
 that pulled in &lt;a href=&#34;https://pypi.org/project/litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;litellm&lt;/a&gt;
 as a transitive dependency. He never ran &lt;code&gt;pip install litellm&lt;/code&gt; himself. The plugin resolved it automatically.&lt;/p&gt;
&lt;p&gt;Shortly after, his machine became unresponsive. RAM exhausted. He traced it to a newly installed litellm package, decoded an obfuscated payload hidden inside it, and &lt;a href=&#34;https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;published the first disclosure&lt;/a&gt;
. His team used &lt;a href=&#34;https://claude.ai/claude-code&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Claude Code&lt;/a&gt;
 to help root-cause the crash.&lt;/p&gt;
&lt;p&gt;The post spread to &lt;a href=&#34;https://www.reddit.com/r/LocalLLaMA/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;r/LocalLLaMA&lt;/a&gt;
, &lt;a href=&#34;https://www.reddit.com/r/Python/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;r/Python&lt;/a&gt;
, and the &lt;a href=&#34;https://news.ycombinator.com&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Hacker News front page&lt;/a&gt;
 within the hour. &lt;a href=&#34;https://x.com/karpathy&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Andrej Karpathy&lt;/a&gt;
 tweeted about it, calling supply chain attacks &amp;ldquo;the most threatening issue in modern software.&amp;rdquo; That tweet crossed 24,000 likes in a day.&lt;/p&gt;
&lt;p&gt;I use LiteLLM in a side project. When I saw the news, I checked my lockfile immediately. Here&amp;rsquo;s everything I found when I dug into what happened, stitched together from &lt;a href=&#34;https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Datadog&lt;/a&gt;
, &lt;a href=&#34;https://snyk.io/articles/poisoned-security-scanner-backdooring-litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Snyk&lt;/a&gt;
, &lt;a href=&#34;https://www.armosec.io/blog/litellm-supply-chain-attack-backdoor-analysis/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Armosec&lt;/a&gt;
, &lt;a href=&#34;https://ramimac.me/teampcp/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;ramimac&amp;rsquo;s incident timeline&lt;/a&gt;
, &lt;a href=&#34;https://www.microsoft.com/en-us/security/blog/2026/03/24/detecting-investigating-defending-against-trivy-supply-chain-compromise/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Microsoft&lt;/a&gt;
, &lt;a href=&#34;https://www.wiz.io/blog/threes-a-crowd-teampcp-trojanizes-litellm&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Wiz&lt;/a&gt;
, and the community threads on &lt;a href=&#34;https://www.reddit.com/r/devops/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;r/devops&lt;/a&gt;
 and &lt;a href=&#34;https://www.reddit.com/r/cybersecurity/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;r/cybersecurity&lt;/a&gt;
.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-is-litellm&#34;&gt;What Is LiteLLM?&lt;/h2&gt;
&lt;p&gt;LiteLLM is a Python library that works like a universal remote for AI APIs. You write one function call, and it routes to OpenAI, Anthropic, Google, Cohere, Mistral, AWS Bedrock, Azure OpenAI, or any of 100+ providers. You give LiteLLM your API keys. It handles the rest.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://pypi.org/project/litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;95 million downloads per month&lt;/a&gt;
 on PyPI. ~40,000 GitHub stars. It&amp;rsquo;s also pulled in automatically by &lt;a href=&#34;https://github.com/stanfordnlp/dspy&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;DSPy&lt;/a&gt;
, &lt;a href=&#34;https://github.com/mlflow/mlflow&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;MLflow&lt;/a&gt;
, and a growing number of agent frameworks, MCP servers, and LLM tools. You can be using LiteLLM without knowing it.&lt;/p&gt;
&lt;p&gt;


  
  
  
    &lt;img src=&#34;https://ashwingopalsamy.in/img/writing/litellm-supply-chain-attack/github-bot-army.png&#34; alt=&#34;GitHub bot army closing issues&#34; loading=&#34;lazy&#34; style=&#34;border-radius: 8px;&#34;&gt;
  

&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;this-started-three-weeks-earlier&#34;&gt;This Started Three Weeks Earlier&lt;/h2&gt;
&lt;p&gt;The LiteLLM backdoor on March 24 was the last move in a campaign that began on &lt;strong&gt;March 1&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A threat actor called &lt;strong&gt;TeamPCP&lt;/strong&gt; submitted a malicious pull request to &lt;a href=&#34;https://github.com/aquasecurity/trivy&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Aqua Security&amp;rsquo;s Trivy&lt;/a&gt;
 repository. Trivy is a widely-used open-source vulnerability scanner - the kind of tool that runs in your CI pipeline to check for security issues. The PR exploited a flaw in Trivy&amp;rsquo;s CI workflow that let the attacker&amp;rsquo;s code run with elevated permissions (a technique called a Pwn Request - basically, a pull request that tricks CI into handing over secrets). This gave them a personal access token.&lt;/p&gt;
&lt;p&gt;Aqua Security responded and rotated credentials, but &lt;a href=&#34;https://ramimac.me/teampcp/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;the rotation wasn&amp;rsquo;t complete&lt;/a&gt;
. Attackers may have captured the new tokens during the rotation. That gap is what enabled everything after.&lt;/p&gt;
&lt;p&gt;


  
  
  
    &lt;img src=&#34;https://ashwingopalsamy.in/img/writing/litellm-supply-chain-attack/malicious-code-analysis.png&#34; alt=&#34;Malicious code analysis&#34; loading=&#34;lazy&#34; style=&#34;border-radius: 8px;&#34;&gt;
  

&lt;/p&gt;
&lt;div class=&#34;diagram-container&#34; data-diagram-type=&#34;mermaid&#34;&gt;
  &lt;div class=&#34;mermaid&#34;&gt;
    
sequenceDiagram
    participant A as Attacker&lt;br/&gt;(TeamPCP)
    participant T as Trivy GitHub&lt;br/&gt;Repo
    participant CI as LiteLLM CI&lt;br/&gt;(GitHub Actions)
    participant P as PyPI&lt;br/&gt;Registry

    Note over A,T: March 1 - Initial Compromise
    A-&gt;&gt;T: Submit malicious PR (Pwn Request)
    T--&gt;&gt;A: CI leaks personal access token
    Note over A,T: Aqua rotates creds,&lt;br/&gt;but rotation incomplete

    Note over A,T: March 19 - Pivot
    A-&gt;&gt;T: Retag trivy-action versions&lt;br/&gt;to point at malicious code

    Note over CI,P: March 24 - Package Takeover
    CI-&gt;&gt;T: Pull trivy-action@latest&lt;br/&gt;(not pinned to SHA)
    T--&gt;&gt;CI: Serve compromised action
    CI--&gt;&gt;A: Leak PyPI publisher token

    A-&gt;&gt;P: Publish litellm v1.82.7&lt;br/&gt;(inline payload)
    A-&gt;&gt;P: Publish litellm v1.82.8&lt;br/&gt;(.pth persistence)

    Note over P: PyPI quarantines&lt;br/&gt;both versions

  &lt;/div&gt;
  &lt;div class=&#34;diagram-actions&#34;&gt;
    &lt;button class=&#34;diagram-action&#34; data-action=&#34;expand&#34; aria-label=&#34;Expand diagram&#34;&gt;
      &lt;svg width=&#34;14&#34; height=&#34;14&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;none&#34; stroke=&#34;currentColor&#34; stroke-width=&#34;2&#34; stroke-linecap=&#34;round&#34; stroke-linejoin=&#34;round&#34;&gt;
        &lt;polyline points=&#34;15 3 21 3 21 9&#34;&gt;&lt;/polyline&gt;
        &lt;polyline points=&#34;9 21 3 21 3 15&#34;&gt;&lt;/polyline&gt;
        &lt;line x1=&#34;21&#34; y1=&#34;3&#34; x2=&#34;14&#34; y2=&#34;10&#34;&gt;&lt;/line&gt;
        &lt;line x1=&#34;3&#34; y1=&#34;21&#34; x2=&#34;10&#34; y2=&#34;14&#34;&gt;&lt;/line&gt;
      &lt;/svg&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id=&#34;how-they-got-the-litellm-pypi-token&#34;&gt;How They Got the LiteLLM PyPI Token&lt;/h2&gt;
&lt;p&gt;LiteLLM&amp;rsquo;s CI pipeline used Trivy to scan for vulnerabilities. Standard practice. But it pulled &lt;a href=&#34;https://github.com/aquasecurity/trivy-action&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;aquasecurity/trivy-action&lt;/a&gt;
 without pinning to a specific commit hash - it used a version tag like &lt;code&gt;@latest&lt;/code&gt; instead of an exact SHA.&lt;/p&gt;
&lt;p&gt;After March 19, every version tag pointed to malicious code. When LiteLLM&amp;rsquo;s CI ran, the compromised Trivy action scraped the GitHub Actions runner environment and found the PyPI publisher token. That token let TeamPCP publish packages as if they were the real LiteLLM maintainer.&lt;/p&gt;
&lt;p&gt;


  
  
  
    &lt;img src=&#34;https://ashwingopalsamy.in/img/writing/litellm-supply-chain-attack/pth-file-backdoor.png&#34; alt=&#34;.pth file backdoor mechanism&#34; loading=&#34;lazy&#34; style=&#34;border-radius: 8px;&#34;&gt;
  

&lt;/p&gt;
&lt;div class=&#34;diagram-container&#34; data-diagram-type=&#34;mermaid&#34;&gt;
  &lt;div class=&#34;mermaid&#34;&gt;
    
graph LR
    A[&#34;Trivy Repo&lt;br/&gt;(compromised)&#34;]:::danger --&gt; B[&#34;GitHub&lt;br/&gt;Bot Army&#34;]:::danger
    B --&gt; C[&#34;PyPI Account&lt;br/&gt;Takeover&#34;]:::danger
    C --&gt; D[&#34;LiteLLM&lt;br/&gt;v1.82.7 / v1.82.8&#34;]:::danger
    D --&gt; E[&#34;.pth&lt;br/&gt;Backdoor&#34;]:::danger
    E --&gt; F[&#34;Credential&lt;br/&gt;Harvest&#34;]:::danger
    F --&gt; G[&#34;Exfil Server&lt;br/&gt;(ICP Canister)&#34;]:::danger

    H[&#34;Trivy Repo&lt;br/&gt;(legitimate)&#34;]:::safe -.-&gt;|compromised| A
    I[&#34;LiteLLM&lt;br/&gt;(legitimate)&#34;]:::safe -.-&gt;|hijacked| D
    J[&#34;PyPI&lt;br/&gt;Registry&#34;]:::safe -.-&gt;|abused| C

    classDef danger fill:#ef4444,color:#fff,stroke:none
    classDef safe fill:#10b981,color:#fff,stroke:none

  &lt;/div&gt;
  &lt;div class=&#34;diagram-actions&#34;&gt;
    &lt;button class=&#34;diagram-action&#34; data-action=&#34;expand&#34; aria-label=&#34;Expand diagram&#34;&gt;
      &lt;svg width=&#34;14&#34; height=&#34;14&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;none&#34; stroke=&#34;currentColor&#34; stroke-width=&#34;2&#34; stroke-linecap=&#34;round&#34; stroke-linejoin=&#34;round&#34;&gt;
        &lt;polyline points=&#34;15 3 21 3 21 9&#34;&gt;&lt;/polyline&gt;
        &lt;polyline points=&#34;9 21 3 21 3 15&#34;&gt;&lt;/polyline&gt;
        &lt;line x1=&#34;21&#34; y1=&#34;3&#34; x2=&#34;14&#34; y2=&#34;10&#34;&gt;&lt;/line&gt;
        &lt;line x1=&#34;3&#34; y1=&#34;21&#34; x2=&#34;10&#34; y2=&#34;14&#34;&gt;&lt;/line&gt;
      &lt;/svg&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id=&#34;what-the-malware-does&#34;&gt;What the Malware Does&lt;/h2&gt;
&lt;p&gt;The payload was &lt;a href=&#34;https://www.armosec.io/blog/litellm-supply-chain-attack-backdoor-analysis/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;triple-nested&lt;/a&gt;
: a base64 blob decodes to an orchestrator script, which decodes a second base64 blob containing the actual harvester. Once running, it goes through six stages:&lt;/p&gt;
&lt;p&gt;


  
  
  
    &lt;img src=&#34;https://ashwingopalsamy.in/img/writing/litellm-supply-chain-attack/credential-harvesting.png&#34; alt=&#34;Credential harvesting paths&#34; loading=&#34;lazy&#34; style=&#34;border-radius: 8px;&#34;&gt;
  

&lt;/p&gt;
&lt;div class=&#34;diagram-container&#34; data-diagram-type=&#34;mermaid&#34;&gt;
  &lt;div class=&#34;mermaid&#34;&gt;
    
graph TD
    ROOT[&#34;.pth Backdoor&#34;]:::danger

    ROOT --&gt; ENV[&#34;ENV Variables&lt;br/&gt;os.environ&#34;]:::warm
    ROOT --&gt; AWS[&#34;~/.aws/credentials&lt;br/&gt;AWS keys&#34;]:::warm
    ROOT --&gt; DOTENV[&#34;.env Files&lt;br/&gt;recursive scan&#34;]:::warm
    ROOT --&gt; PROC[&#34;/proc/self/environ&lt;br/&gt;process secrets&#34;]:::warm
    ROOT --&gt; EXFIL[&#34;Outbound HTTP&lt;br/&gt;Exfil to ICP canister&#34;]:::warm

    ENV --&gt; EXFIL
    AWS --&gt; EXFIL
    DOTENV --&gt; EXFIL
    PROC --&gt; EXFIL

    classDef danger fill:#ef4444,color:#fff,stroke:none
    classDef warm fill:#f59e0b,color:#fff,stroke:none

  &lt;/div&gt;
  &lt;div class=&#34;diagram-actions&#34;&gt;
    &lt;button class=&#34;diagram-action&#34; data-action=&#34;expand&#34; aria-label=&#34;Expand diagram&#34;&gt;
      &lt;svg width=&#34;14&#34; height=&#34;14&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;none&#34; stroke=&#34;currentColor&#34; stroke-width=&#34;2&#34; stroke-linecap=&#34;round&#34; stroke-linejoin=&#34;round&#34;&gt;
        &lt;polyline points=&#34;15 3 21 3 21 9&#34;&gt;&lt;/polyline&gt;
        &lt;polyline points=&#34;9 21 3 21 3 15&#34;&gt;&lt;/polyline&gt;
        &lt;line x1=&#34;21&#34; y1=&#34;3&#34; x2=&#34;14&#34; y2=&#34;10&#34;&gt;&lt;/line&gt;
        &lt;line x1=&#34;3&#34; y1=&#34;21&#34; x2=&#34;10&#34; y2=&#34;14&#34;&gt;&lt;/line&gt;
      &lt;/svg&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id=&#34;two-versions-two-triggers&#34;&gt;Two Versions, Two Triggers&lt;/h2&gt;
&lt;p&gt;Version 1.82.7 injected the payload at line 128 of &lt;code&gt;litellm/proxy/proxy_server.py&lt;/code&gt;, between two unrelated legitimate code blocks. It runs when your code imports the LiteLLM proxy module.&lt;/p&gt;
&lt;p&gt;Version 1.82.8 did something more dangerous. It added a file called &lt;code&gt;litellm_init.pth&lt;/code&gt; (34,628 bytes). In Python, &lt;code&gt;.pth&lt;/code&gt; files are a little-known feature: any file with that extension in the packages directory gets executed &lt;em&gt;every time Python starts&lt;/em&gt;. Not when you import something. Not when you run a script. &lt;strong&gt;Every single Python process.&lt;/strong&gt;&lt;/p&gt;


&lt;div class=&#34;callout callout--insight&#34;&gt;
  &lt;div class=&#34;callout-icon&#34;&gt;★&lt;/div&gt;
  &lt;div class=&#34;callout-body&#34;&gt;
    Most developers don&amp;rsquo;t know &lt;code&gt;.pth&lt;/code&gt; files can execute arbitrary code. Originally designed for adding directories to &lt;code&gt;sys.path&lt;/code&gt;, any line in a &lt;code&gt;.pth&lt;/code&gt; file starting with &lt;code&gt;import&lt;/code&gt; is executed by CPython&amp;rsquo;s &lt;code&gt;site.py&lt;/code&gt; at startup. This means a malicious &lt;code&gt;.pth&lt;/code&gt; file in your &lt;code&gt;site-packages&lt;/code&gt; directory runs code before your application even begins, on every Python invocation: &lt;code&gt;pytest&lt;/code&gt;, your IDE&amp;rsquo;s language server, even &lt;code&gt;pip install&lt;/code&gt;. CPython maintainers have acknowledged the risk. No patch exists.
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;


  
  
  
    &lt;img src=&#34;https://ashwingopalsamy.in/img/writing/litellm-supply-chain-attack/persistence-mechanism.png&#34; alt=&#34;Persistence mechanism&#34; loading=&#34;lazy&#34; style=&#34;border-radius: 8px;&#34;&gt;
  

&lt;/p&gt;
&lt;div class=&#34;diagram-container&#34; data-diagram-type=&#34;mermaid&#34;&gt;
  &lt;div class=&#34;mermaid&#34;&gt;
    
graph TD
    A[&#34;Python Starts&#34;]:::info --&gt; B[&#34;Scans site-packages&lt;br/&gt;for .pth files&#34;]:::neutral
    B --&gt; C[&#34;Finds litellm_init.pth&#34;]:::danger
    C --&gt; D{&#34;Line starts&lt;br/&gt;with import?&#34;}:::neutral
    D --&gt;|Yes| E[&#34;Executes code&lt;br/&gt;via site.py&#34;]:::danger
    E --&gt; F[&#34;Decodes base64&lt;br/&gt;orchestrator&#34;]:::danger
    F --&gt; G[&#34;Decodes base64&lt;br/&gt;harvester&#34;]:::danger
    G --&gt; H[&#34;Scrapes credentials&lt;br/&gt;from ENV, files, /proc&#34;]:::danger
    H --&gt; I[&#34;Exfiltrates to&lt;br/&gt;ICP canister&#34;]:::danger

    D --&gt;|No| J[&#34;Adds path to&lt;br/&gt;sys.path (normal)&#34;]:::safe

    E --&gt; K[&#34;Spawns child&lt;br/&gt;Python process&#34;]:::danger
    K --&gt;|&#34;.pth fires again&#34;| A

    classDef info fill:#6366f1,color:#fff,stroke:none
    classDef neutral fill:#64748b,color:#fff,stroke:none
    classDef danger fill:#ef4444,color:#fff,stroke:none
    classDef safe fill:#10b981,color:#fff,stroke:none

  &lt;/div&gt;
  &lt;div class=&#34;diagram-actions&#34;&gt;
    &lt;button class=&#34;diagram-action&#34; data-action=&#34;expand&#34; aria-label=&#34;Expand diagram&#34;&gt;
      &lt;svg width=&#34;14&#34; height=&#34;14&#34; viewBox=&#34;0 0 24 24&#34; fill=&#34;none&#34; stroke=&#34;currentColor&#34; stroke-width=&#34;2&#34; stroke-linecap=&#34;round&#34; stroke-linejoin=&#34;round&#34;&gt;
        &lt;polyline points=&#34;15 3 21 3 21 9&#34;&gt;&lt;/polyline&gt;
        &lt;polyline points=&#34;9 21 3 21 3 15&#34;&gt;&lt;/polyline&gt;
        &lt;line x1=&#34;21&#34; y1=&#34;3&#34; x2=&#34;14&#34; y2=&#34;10&#34;&gt;&lt;/line&gt;
        &lt;line x1=&#34;3&#34; y1=&#34;21&#34; x2=&#34;10&#34; y2=&#34;14&#34;&gt;&lt;/line&gt;
      &lt;/svg&gt;
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Running &lt;code&gt;pytest&lt;/code&gt; starts Python - payload fires. Your IDE&amp;rsquo;s language server starts Python - same. Even &lt;code&gt;pip install&lt;/code&gt; triggers it. In CI/CD, the payload runs during build steps, not just at application runtime. This maps to &lt;a href=&#34;https://attack.mitre.org/techniques/T1546/018/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;MITRE ATT&amp;amp;CK T1546.018&lt;/a&gt;
 (Python Startup Hooks).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.pth&lt;/code&gt; mechanism also caused an accidental &lt;strong&gt;fork bomb&lt;/strong&gt;: the malware spawned a child Python process, which triggered &lt;code&gt;.pth&lt;/code&gt; again, which spawned another child, and so on. Exponential process creation until the system ran out of memory. &lt;a href=&#34;https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;FutureSearch called it &amp;ldquo;a bug in the malware&amp;rdquo;&lt;/a&gt;
 - and it&amp;rsquo;s the bug that led to the discovery.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-discovery-and-the-bot-army&#34;&gt;The Discovery and the Bot Army&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;McMahon published on FutureSearch&amp;rsquo;s blog&lt;/a&gt;
. The disclosure spread to &lt;a href=&#34;https://www.reddit.com/r/LocalLLaMA/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;r/LocalLLaMA&lt;/a&gt;
, &lt;a href=&#34;https://www.reddit.com/r/Python/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;r/Python&lt;/a&gt;
, and the &lt;a href=&#34;https://news.ycombinator.com&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Hacker News front page&lt;/a&gt;
 within the hour. Then things got strange.&lt;/p&gt;
&lt;p&gt;When the community opened &lt;a href=&#34;https://github.com/BerriAI/litellm/issues/24512&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;GitHub issue #24512&lt;/a&gt;
 to discuss the compromise, TeamPCP deployed &lt;strong&gt;88 bot comments from 73 unique accounts in a 102-second window&lt;/strong&gt; (12:44-12:46 UTC). These were previously compromised developer accounts, not fresh ones. &lt;a href=&#34;https://snyk.io/articles/poisoned-security-scanner-backdooring-litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Snyk found 76% overlap&lt;/a&gt;
 with the botnet used during the Trivy disclosure days earlier. The comments were a mix of generic praise and troll content (&amp;ldquo;sugma&amp;rdquo;, &amp;ldquo;ligma&amp;rdquo;), designed to bury the technical discussion.&lt;/p&gt;
&lt;p&gt;Then, using the stolen LiteLLM maintainer account, they closed &lt;a href=&#34;https://github.com/BerriAI/litellm/issues/24512&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;issue #24512&lt;/a&gt;
 as &lt;strong&gt;&amp;ldquo;not planned.&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The community opened a parallel tracking issue. &lt;a href=&#34;https://pypi.org/project/litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;PyPI&lt;/a&gt;
 quarantined both versions. The real LiteLLM maintainer &lt;a href=&#34;https://news.ycombinator.com&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;confirmed on HN&lt;/a&gt;
 that all GitHub, Docker, and PyPI keys had been rotated and accounts moved to new identities.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-makes-this-worse-than-usual&#34;&gt;What Makes This Worse Than Usual&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The target was a credential vault.&lt;/strong&gt; LiteLLM holds more API keys per deployment than almost any other library. A typical setup has keys for OpenAI, Anthropic, Google, Azure, Hugging Face, Bedrock, plus cloud credentials, database passwords, and whatever MCP server access you&amp;rsquo;ve configured. As &lt;a href=&#34;https://www.armosec.io/blog/litellm-supply-chain-attack-backdoor-analysis/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Armosec put it&lt;/a&gt;
: &amp;ldquo;AI tooling is becoming the fattest, most credential-rich target in your entire infrastructure.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transitive dependency exposure.&lt;/strong&gt; &lt;a href=&#34;https://futuresearch.ai/blog/no-prompt-injection-required/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;The FutureSearch developer never installed LiteLLM.&lt;/a&gt;
 It came in through a Cursor MCP plugin. &lt;a href=&#34;https://github.com/stanfordnlp/dspy&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;DSPy&lt;/a&gt;
 pulls it in. &lt;a href=&#34;https://github.com/mlflow/mlflow&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;MLflow&lt;/a&gt;
 pulls it in. You can be in the blast radius without choosing to use the library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unseizable infrastructure.&lt;/strong&gt; TeamPCP&amp;rsquo;s command server includes an ICP canister replicated across 13 nodes in 10 countries. &lt;a href=&#34;https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-teampcp-supply-chain-campaign/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Datadog documented this as the first observed use of ICP as a command server&lt;/a&gt;
 in a supply chain campaign. By &lt;a href=&#34;https://ramimac.me/teampcp/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;version 3.3 of their kamikaze.sh payload&lt;/a&gt;
, they were hiding Python code inside WAV audio files using steganography to bypass detection filters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Organized cover-up.&lt;/strong&gt; Bot armies from a pre-existing botnet (76% account reuse), troll comments, and closing the disclosure issue using the stolen maintainer account. This is not a lone actor.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;if-you-installed-1827-or-1828&#34;&gt;If You Installed 1.82.7 or 1.82.8&lt;/h2&gt;


&lt;div class=&#34;callout callout--warning&#34;&gt;
  &lt;div class=&#34;callout-icon&#34;&gt;⚠&lt;/div&gt;
  &lt;div class=&#34;callout-body&#34;&gt;
    &lt;strong&gt;Check your environment immediately.&lt;/strong&gt; If any of the following commands return results, the payload has already executed. Upgrading the package alone is not enough.
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Backdoor persistence&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ls ~/.config/sysmon/sysmon.py 2&amp;gt;/dev/null &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;BACKDOOR FOUND&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl --user status sysmon.service 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# .pth file (v1.82.8)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;find &lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;python3 -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;import site; print(&amp;#39; &amp;#39;.join(site.getsitepackages()))&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -name &lt;span class=&#34;s2&#34;&gt;&amp;#34;litellm_init.pth&amp;#34;&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Check uv caches too&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;find ~/.cache/uv -name &lt;span class=&#34;s2&#34;&gt;&amp;#34;litellm_init.pth&amp;#34;&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Exfil artifacts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ls /tmp/tpcp.tar.gz /tmp/session.key /tmp/payload.enc /tmp/.pg_state 2&amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Kubernetes spread&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;kubectl get pods --all-namespaces &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep node-setup
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If anything shows up, &lt;strong&gt;upgrading the package is not enough.&lt;/strong&gt; The payload already ran!&lt;/p&gt;


&lt;div class=&#34;callout callout--warning&#34;&gt;
  &lt;div class=&#34;callout-icon&#34;&gt;⚠&lt;/div&gt;
  &lt;div class=&#34;callout-body&#34;&gt;
    &lt;p&gt;&lt;strong&gt;Rotate immediately:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All LLM provider API keys (OpenAI, Anthropic, Google, every key LiteLLM proxied)&lt;/li&gt;
&lt;li&gt;Cloud credentials reachable from that runtime (AWS, GCP, Azure)&lt;/li&gt;
&lt;li&gt;GitHub and PyPI publishing tokens&lt;/li&gt;
&lt;li&gt;CI/CD secrets&lt;/li&gt;
&lt;li&gt;SSH keys&lt;/li&gt;
&lt;li&gt;Kubernetes service account tokens&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Then rebuild.&lt;/strong&gt; Known-good images, pinned dependencies. Audit transitive dependencies in every project that uses LiteLLM. The last known-clean version is &lt;strong&gt;1.82.6&lt;/strong&gt;.&lt;/p&gt;

  &lt;/div&gt;
&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id=&#34;what-im-sitting-with&#34;&gt;What I&amp;rsquo;m Sitting With&lt;/h2&gt;
&lt;p&gt;The attack on &lt;a href=&#34;https://github.com/BerriAI/litellm/issues/24512&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;GitHub issue #24512&lt;/a&gt;
 spawned a Hacker News thread asking &lt;a href=&#34;https://news.ycombinator.com&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;&amp;ldquo;What are you using to run dev environments safely?&amp;rdquo;&lt;/a&gt;
 That&amp;rsquo;s the right question to come out of this.&lt;/p&gt;
&lt;p&gt;Consider the shape of a modern AI agent deployment: LLM provider keys for billing and access, tool credentials for SaaS integrations, MCP server access that can reach Slack, GitHub, and production infrastructure, vector databases with proprietary data, memory stores with conversation history. All of it in env vars, &lt;code&gt;.env&lt;/code&gt; files, and Kubernetes Secrets. All of it accessible to any process in the runtime.&lt;/p&gt;
&lt;p&gt;TeamPCP chose their targets in order: &lt;a href=&#34;https://github.com/aquasecurity/trivy&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Trivy&lt;/a&gt;
 (security scanner), &lt;a href=&#34;https://github.com/Checkmarx/kics-github-action&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;Checkmarx&lt;/a&gt;
 (code analysis), then &lt;a href=&#34;https://pypi.org/project/litellm/&#34; rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;&gt;LiteLLM&lt;/a&gt;
 (AI API proxy). Each one has elevated trust and broad credential access. The tools that check your code and route your AI requests have the widest blast radius when compromised, because we hand them the keys to everything.&lt;/p&gt;
&lt;p&gt;Projects pin application dependencies. They rarely pin the tools that run in CI alongside them. &lt;code&gt;trivy-action@v0.20.0&lt;/code&gt; and &lt;code&gt;trivy-action@latest&lt;/code&gt; pointed to different code on March 19. That distinction is what separates &amp;ldquo;compromised&amp;rdquo; from &amp;ldquo;unaffected.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Anyways, it was fun running into the blogs and reading RCA of this incident. Just wanted to run through it for everyone here.&lt;/p&gt;
&lt;p&gt;Thanks for your time and reading this piece.&lt;/p&gt;
&lt;p&gt;Best,
Ashwin.&lt;/p&gt;
</content:encoded>
      <author>ashwin@ashwingopalsamy.in (Ashwin Gopalsamy)</author>
    </item>
    
  </channel>
</rss>
