lightning PyPI Compromise: A Bun-Based Credential Stealer in Python
April 30, 2026
0 mins readOn 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-2568046a11187c135da6959862ff3846e99ad15462d2ec8a2f77a30ad53ebd5dcf2): a small Python downloader. It fetches Bunv1.3.13fromhttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/<platform>.zipand 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-2565f5852b5f604369945118937b058e49064612ac69826e0adadca39a357dfb5b1): an ~11 MB single-line obfuscated JavaScript file. The obfuscation isjavascript-obfuscator-style string-array rotation, with a secondary cipher named__decodeScrambled()(PBKDF2/SHA-256, 200,000 iterations, saltctf-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:
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 athttp://169.254.169.254(AWS IMDS),http://169.254.170.2(AWS ECS),https://oauth2.googleapis.com/tokeninfo, and validates harvested tokens againsthttps://api.github.com/userandhttps://registry.npmjs.org/-/whoami.Repository poisoning through GitHub's GraphQL
createCommitOnBranchmutation, signing commits asclaude <claude@users.noreply.github.com>with aCo-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 directPUTtoregistry.npmjs.orgwithout 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.2git tag exists onLightning-AI/pytorch-lightningand was created on March 19 byjustusschock, but the correspondingrelease-pkgworkflow run failed at thepublish-packagesstep. 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.3git tag does not exist at all. There is norefs/tags/2.6.3, no GitHub Release, and no workflow run associated with that version. Yet a wheel forlightning==2.6.3was 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 |
|---|---|---|
| Create / Delete branch |
|
| Create / Delete branch |
|
| Issue filed | #21689 by |
| Issue closed | #21689 closed by |
| Issue filed | #21690 by |
| Create / Delete branch |
|
| Create / Delete branch |
|
| Issue closed | #21690 closed by |
| Issue filed | #21691 by |
| Issue closed | #21691 closed by |
| Issue filed | #21692 by |
| Issue closed | #21692 closed by |
| Issue closed | #21691 closed again |
| Comment | Maintainer |
| Issue closed | #21691 closed a third time |
A few details stand out from this sequence:
The four random branches (
pgzicpysge,hwofzwmrto,uwpkpcguba, plusdependabot/fix-deds) follow a 10-character lowercase pattern previously associated with Shai-Hulud's worm-style write-access probing. All five branches were deleted within seconds and triggered no workflow runs, suggesting branch protection on default branches blocked direct pushes.The
dependabot/fix-dedsbranch uses a slash separator and a "deds" segment that does not match Lightning-AI's actual Dependabot configuration (which uses a dash prefix for real Dependabot pushes), so the branch name does not match a legitimate Dependabot push for this repository.Issue #21692 was filed by
pvdzafter watching three issues get closed in a row. Its body reads, in full: "@Borda @williamFalcon @awaelchli see https://github.com/Lightning-AI/pytorch-lightning/issues/21691. Issues are auto-closed by the attacker."pvdzis Peter van der Zee, an engineer at Socket and the credit on the Snyk advisory; he filed three of the four disclosure issues directly.Issue #21691 was closed three separate times, then reopened three times by maintainer
ethanwharris. Multiple comments in the thread were deleted byLightning-AIas an org principal, consistent with maintainers moderating content posted by the suspected-compromised account. The issue remains open at the time of writing.
The combination of disclosure-thread closures, branch creation patterns previously associated with the npm worm, and the Dependabot-styled branch name is consistent with a single set of compromised credentials being used both to publish to PyPI and to act on the GitHub repository. The maintainer-credential entry point is a recurring pattern in open source supply chain attacks Snyk has covered before, including the npm maintainer-compromise wave and the elementary-data PyPI compromise that targeted data-engineering credentials.
A separate piece of context: the GitHub issue thread also contained an onion link to a Team PCP-branded site, which posts a PGP-signed message claiming connections to LAPSUS$ and earlier extortion activity. The PGP signature has not been independently verified, and the underlying claims are unconfirmed. The same Team PCP branding appeared in a separate Snyk-tracked PyPI compromise earlier this year, the litellm backdoor on March 24, 2026. There is also notable counter-evidence in the lineage: in the April 22 xinference PyPI compromise, the Team PCP X account publicly denied involvement and pointed to a copycat using the brand. Different vendors split on the lightning attribution today; Wiz assesses with high confidence that the broader campaign is the same operator (citing a shared RSA public key used to wrap exfiltrated secrets), Aikido frames this attack as a continuation of "Mini Shai-Hulud," and Socket assesses it as a distinct actor employing a similar playbook. Whether the connection is real, opportunistic, or a deliberate false flag is still open.
Why "Bun in Python" matters
The most informative forensic detail is the runtime choice. The payload that runs after import lightning is JavaScript, executed under Bun, on a Python developer's machine. A Python-targeting credential stealer does not need a JavaScript runtime, and bundling Bun materially increases the wheel size. The most parsimonious explanation is payload reuse: the same router_runtime.js-style stealer that the Mini Shai-Hulud variant ran inside preinstall hooks on npm yesterday is now being delivered into Python projects through a small Python wrapper that bootstraps Bun and the JS blob. The cipher signature in router_runtime.js (function name __decodeScrambled, PBKDF2/SHA-256 with 200,000 iterations and salt ctf-scramble-v2) is identical to the SAP execution.js, Bitwarden CLI, and Checkmarx KICS payloads, indicating shared tooling rather than independent reimplementation.
This is consistent with the broader Shai-Hulud lineage. The original Shai-Hulud worm in September 2025, the SHA1-Hulud follow-on wave in November 2025 that hit 600+ packages, the Holiday Whisper / Shai-Hulud 3.0 variant at year-end, and Snyk's post-mortem on the resilience lessons all pointed at the same underlying capability: a worm-style JavaScript stealer that harvests tokens, validates them against registry.npmjs.org, and uses any npm-write scope it finds to publish itself outward.

Shai-Hulud NPM Attack: Remediation with Snyk. Short walkthrough on identifying and remediating Shai-Hulud-affected dependencies using snyk test and the Snyk Security Database.
The lightning compromise extends that capability to PyPI without rewriting the stealer. Wrapping a JavaScript payload in a Python loader keeps the JS-ecosystem tooling intact while expanding registry reach. The cadence of the last 48 hours is consistent with that read: yesterday npm, today PyPI, with the same payload shape and a comparable detection window.
Recommended actions
Treat any system that installed and imported lightning==2.6.2 or lightning==2.6.3 as compromised. The execution chain runs at import time, not just at install time, so import alone is enough to trigger the payload.
Pin or remove the bad versions. Block
lightning==2.6.2andlightning==2.6.3in your registry mirrors and downgrade tolightning==2.6.1(clean release from January 30, 2026). Do not upgrade past2.6.1until the maintainers publish a confirmed clean version.Rotate credentials reachable from the affected environment. GitHub Personal Access Tokens, GitHub fine-grained tokens, npm tokens, cloud provider keys (AWS, GCP, Azure), and any secrets present in environment variables on the affected host. Rotate from a separate, trusted machine.
Audit GitHub for unauthorized commits. The payload commits to victim repositories using the spoofed identity
claude <claude@users.noreply.github.com>. Check repository activity for commits matching that author, branch creations, or workflow file changes under any account whose tokens were exposed.Audit CI/CD logs and developer machines. Look for outbound connections to
https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/, unexpectedbunprocesses, calls tohttp://169.254.169.254orhttp://169.254.170.2from machines that should not be probing cloud metadata, and new daemon threads spawned by Python interpreters that importedlightningin the affected window.Review npm tarballs on developer machines. The payload includes code capable of mutating local npm packages and publishing them to
registry.npmjs.orgwithout invoking the npm CLI. If a developer machine had npm credentials in scope and importedlightning, treat the npm token as exposed and audit recent publishes from accounts associated with that machine.Search GitHub for dead-drop repositories. Across the broader campaign, exfiltration goes to victim-account-owned public repositories with the description string "A Mini Shai-Hulud has Appeared" and commit messages prefixed
OhNoWhatsGoingOnWithGitHub:. The queryhttps://github.com/search?q=%22A+Mini+Shai-Hulud+has+Appeared%22&type=repositoriesreturns the live set.Check Snyk findings. Run
snyk testagainst affected projects; advisory SNYK-PYTHON-LIGHTNING-16323121 flags both malicious versions directly.
What this tells us about the threat model
A few observations from this incident, beyond the immediate cleanup work.
Attacker tooling is being reused on a faster cadence than registry takedown can keep up with. The malicious lightning versions were detected by automated scanning within roughly 18 minutes of publication, while the package remained installable and the disclosure thread on GitHub had been closed. The 24-hour gap between yesterday's npm SAP CAP campaign and today's PyPI lightning release is consistent with either a single operator iterating across registries or a copycat reusing the same payload while it remains functional.
Cross-ecosystem payload reuse via embedded runtimes is a recurring pattern. Snyk has covered Bun-loader-plus-large-obfuscated-payload variants in npm twice in the last week, and now in PyPI. The structural takeaway is that the underlying credential theft, GitHub repository abuse, and npm self-propagation logic are the same regardless of which registry shipped the wheel; treating each registry's compromise as a distinct response track misses the shared post-exploitation surface.
The publishing-path failure mode is now explicit. A long-lived PyPI API token stored in GitHub Actions secrets, with no Trusted Publisher binding and no manual approval gate, lets a credential-theft incident on any developer or CI host with access to that secret turn into a registry compromise without ever touching the legitimate workflow. Both this incident and the SAP cap-js compromise point at the same fix: trusted-publisher OIDC binding, separate principals for repository administration and registry publishing, and an explicit human approval before releases reach the registry. Snyk's post-mortem on Shai-Hulud resilience covers the broader set of controls. Snyk continues to track the broader Shai-Hulud lineage and related supply chain incidents on the Snyk Vulnerability Database, and the live lightning advisory will be updated as the payload is fully deobfuscated and additional indicators of compromise are confirmed.
Start securing your Python apps
Find and fix Python vulnerabilities with Snyk for free.
No credit card required.
Or Sign up with Azure AD Docker ID Bitbucket
By using Snyk, you agree to abide by our policies, including our Terms of Service and Privacy Policy.
