A Python developer ran import lightning in a Jupyter notebook on April 30, 2026, and unknowingly downloaded a JavaScript runtime from GitHub to execute an 11 MB credential stealer on their laptop. That is the actual execution path of the compromised lightning package on PyPI — a deep learning framework that, according to pypistats.org, ships 311,027 downloads a day. The attackers did not bother rewriting their stealer in Python. They wrapped a JavaScript payload in a thin Python loader, shipped Bun alongside it, and let __init__.py do the rest. The supply chain threat model just collapsed across two ecosystems, and most security tooling is still scoped to one.
A JavaScript Stealer Ported to Python Without Being Rewritten
On April 30, 2026, two malicious releases of lightning — versions 2.6.2 and 2.6.3 — were published to PyPI, per Snyk advisory SNYK-PYTHON-LIGHTNING-16323121 (CVSS 4.0 base score 9.3, CWE-506, credit Peter van der Zee). The wheels added a hidden _runtime directory containing start.py, which fetches Bun v1.3.13 from GitHub.com/oven-sh/bun/releases, and router_runtime.js, an ~11 MB obfuscated JavaScript stealer wired into module import via a daemon thread in __init__.py. The last clean release, 2.6.1, dates to January 30, 2026.
The forensic detail that matters most is not the wheel layout. It is that the JavaScript blob inside is the same artifact Snyk documented one day earlier in the Mini Shai-Hulud npm wave that hit SAP CAP packages. The cipher in router_runtime.js — function name __decodeScrambled, PBKDF2/SHA-256 with 200,000 iterations and salt ctf-scramble-v2 — is byte-identical to payloads recovered from the Checkmarx and Bitwarden CLI compromises earlier this year. The attackers chose payload reuse over native Python tooling because rewriting a credential stealer costs days, while shipping a 90 MB Bun binary inside a wheel costs nothing.
If your team runs CI jobs that import lightning just to print its __version__ for a build manifest — a pattern common in MLOps pipelines and ML-driven healthcare diagnostics workflows — those runners executed the payload. Import alone is enough; there is no postinstall hook and no separate command. Expect more JavaScript stealers wearing Python wrappers (and vice versa) over the next quarter. Single-ecosystem SCA scanners that treat npm and PyPI as separate problem domains are about to look very dated.
The Stored PyPI Token Was the Real Vulnerability
The lightning project’s release-pkg.yml workflow publishes to PyPI using a long-lived API token (secrets.PYPI_TOKEN_LIGHTNING) via pypa/gh-action-pypi-publish. There is no PyPI Trusted Publisher (OIDC) binding, no per-publish approval gate. The 2.6.2 git tag exists on Lightning-AI/pytorch-lightning but its corresponding publish workflow run failed — issue #21681 from April 20 confirms 2.6.2 was missing from PyPI for six weeks. The 2.6.3 tag does not exist at all. Yet both wheels reached PyPI.
The simplest reading is that the attacker held the stored token and uploaded both wheels directly via twine without ever touching the GitHub Actions workflow. The six-week PyPI absence even provided cover: developers waiting for the long-delayed release had no reason to flag it as suspicious when it finally appeared. SAP disclosed the same structural failure yesterday in cap-js/cds-dbs — publish permission held in workflow secrets without a manual approval gate.
Consider a startup with a single SRE who set up PyPI publishing two years ago. The token sits in a GitHub Actions secret. Anyone who phishes that one engineer, or compromises any other CI workflow with access to that secret, owns the registry endpoint without needing to push a commit, run a workflow, or trigger a release. The fix is well-known and unglamorous: Trusted Publisher OIDC binding, separate principals for repo administration and registry publishing, and a human approval step before releases ship. Within the next year, expect PyPI and npm to start nudging — then forcing — major projects off long-lived tokens. The maintainers who delay the migration will be the next case studies.
Import-Time Execution and a Disclosure Thread Closed by the Attacker
The most uncomfortable detail of the incident is how the disclosure unfolded. Between 12:40Z and 14:12Z on April 30, four community-filed issues on Lightning-AI/pytorch-lightning were closed within minutes by the pl-ghost GitHub account — a CI service account whose PAT_GHOST token is referenced directly in release-pkg.yml for cross-repo automation. Issue #21691 was closed three separate times before maintainer ethanwharris reopened it. Issue #21692, filed by pvdz (Peter van der Zee, the Snyk advisory credit), reads in full: “Issues are auto-closed by the attacker.”
In parallel, four random 10-character lowercase branches (pgzicpysge, hwofzwmrto, uwpkpcguba) plus a fake dependabot/fix-deds branch were created and deleted within seconds across litAI, utilities, and torchmetrics. The naming convention matches worm-style write-access probing seen in earlier Shai-Hulud waves. The dependabot/fix-deds branch uses a slash separator that does not match the repo’s real Dependabot configuration — a tell that someone with pl-ghost’s token was testing branch protection on every Lightning-AI repository they could reach.
The attacker had publishing credentials, repository write access via a service account, and the ability to suppress inbound disclosure for nearly 90 minutes. The payload itself runs at import time via a daemon thread that suppresses stdout and stderr, so a developer importing the package in a Jupyter cell or a CI step sees nothing. That asymmetry is what defenders need to account for: detection and response infrastructure assumed installation as the trigger point, with pip install logs and postinstall hooks as observable signals. Import-time execution sidesteps both. PyPI’s runtime monitoring has to catch up; process-level eBPF tracing on developer machines and CI runners is the only credible answer.
Cross-Ecosystem Worms Are the New Default Threat Shape
The lightning payload’s third execution stage mutates local npm tarballs on the developer’s machine, injects setup.mjs, bumps the patch version, and publishes via direct PUT to registry.npmjs.org without invoking the npm CLI. A Python package compromise can now seed an npm worm. The original Shai-Hulud in September 2025, the SHA1-Hulud follow-on in November 2025 that hit 600+ packages, the Holiday Whisper variant at year-end, yesterday’s SAP CAP campaign, today’s lightning — these are not separate incidents. They are iterations of one tooling stack against successive registries.
Vendor attribution is split. 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 lightning as a continuation of Mini Shai-Hulud. Socket calls it a distinct actor running a similar playbook. The GitHub thread also surfaced a Team PCP-branded onion link with a PGP-signed message claiming LAPSUS$ connections — the same brand that appeared in the litellm backdoor on March 24, 2026, but which the Team PCP X account publicly disowned during the April 22 xinference PyPI compromise. Whether the lineage is one operator, copycats, or a deliberate false flag, the practical answer is the same: the end-to-end traceability problems that supply chain software already solves for physical goods need a software-registry equivalent, and SBOMs alone are not it.
Within six months, at least one Tier 1 registry will roll out mandatory short-lived OIDC-only publishing for the top 1,000 packages by download count; the holdouts will absorb the next several waves of this campaign. The economics favor the attacker until that happens — payload reuse keeps marginal cost per ecosystem near zero, while remediation cost stays high.
FAQ
Q: What versions of lightning are compromised, and what should I pin to?
A: Versions 2.6.2 and 2.6.3 are malicious. The last clean release is 2.6.1 from January 30, 2026. Block the bad versions in registry mirrors and downgrade. The legacy pytorch-lightning package is unaffected and still resolves to its clean 2.6.1. Do not upgrade past 2.6.1 until maintainers publish a verified clean release.
Q: I imported lightning in a CI job. Was I exposed even if I did not run training code?
A: Yes. The payload is wired into __init__.py via a daemon thread, so any process that ran import lightning against 2.6.2 or 2.6.3 triggered the credential stealer. A one-line script that imported the package to read its version is enough. Treat the host as compromised, rotate every credential reachable from the environment, and audit GitHub for commits authored by the spoofed identity claude <claude@users.noreply.GitHub.com>.
Q: How does a JavaScript runtime end up executing a Python package’s payload?
A: The malicious wheel includes a hidden _runtime/start.py that downloads Bun v1.3.13 from GitHub at import time and uses it to execute an obfuscated JavaScript stealer (router_runtime.js). The attackers reused an existing JavaScript payload from the npm Mini Shai-Hulud campaign rather than rewriting it in Python, which is why a Python developer ends up running a JavaScript runtime they never installed.
Key Takeaways
- Single-ecosystem SCA scanners miss cross-ecosystem payloads. If your tooling treats PyPI and npm advisories as separate streams, the next Bun-in-Python or Python-in-npm variant will land before you correlate the indicators.
- Long-lived registry tokens stored in CI secrets are now the dominant entry point for supply chain compromise. Migrate to OIDC-based Trusted Publishers and require manual approval gates before any release reaches a public registry.
- Import-time execution is the new default for malicious packages. Detection that relies on
pip installlogs orpostinstallhooks will quietly stop working — invest in runtime process tracing on CI runners and developer workstations. - Service accounts with cross-repo write tokens are a single-point-of-failure for both publishing and disclosure handling. Separate publishing principals from automation principals, and never let one compromised token close issues and push wheels.
- Expect attacker dwell time on compromised registry credentials to shrink as detection improves, but expect the cadence of cross-registry waves to accelerate in parallel — yesterday’s npm campaign and today’s PyPI release were 24 hours apart with the same payload.