GitLab's Vulnerability Research team has identified a coordinated supply chain attack on PyPI deploying a copy of the Shai-Hulud malware. We found five malicious packages: four typosquats impersonating Flask, Requests, and NumPy, and one weaponized legitimate project. The packages execute code at install time, with no import or function call required, and carry a self-propagating credential stealer that targets CI/CD environments across all major cloud providers.
We confirmed that GitLab was not using any of the affected packages and are sharing our findings to help the broader security community respond effectively.
Inside the attack
Our monitoring systems flagged five malicious PyPI packages from a single account (elitexp) on June 7, 2026. Four are typosquats:
- rlask and tlask, typosquats of Flask
- rsquests, a typosquat of Requests
- nhmpy, a typosquat of NumPy
The fifth, mflux-streamlit, is a legitimate project with real users that the attacker weaponized by publishing malicious versions 0.0.3 and 0.0.4 after the typosquat wave.
The attacker published clean "probe" versions first, with version numbers matching the real latest releases exactly (Flask 3.1.3, Requests 2.34.2, and NumPy 2.4.6). Once these were indexed without issue, the attacker pushed new versions with the worm payload baked in.
This is a copycat deployment. TeamPCP, the group behind Shai-Hulud, open-sourced the worm's code on May 12, 2026. We've been tracking independent actors picking up the toolkit and aiming it at new targets since then. This campaign brings the same worm to the Python ecosystem.
Technical analysis
Initial infection vector
The original npm variant used a preinstall script. This campaign takes a different approach, exploiting Python's .pth file mechanism. Wheel packages can ship .pth files that Python processes automatically on startup, requiring no explicit import. Each malicious package includes a file like rlask-setup.pth containing a one-liner dropper:
The dropper checks for a marker file (.bun_ran in the system temp directory) to avoid re-execution, then downloads the Bun JavaScript runtime from GitHub and uses it to execute a 5 MB obfuscated JavaScript payload bundled in the package.
Early versions of rlask also included a sitecustomize.py file as a backup execution path. Python auto-imports sitecustomize on startup, and this file searched sys.path for the hidden _index.js payload:
The attacker dropped this backup mechanism in later versions, apparently finding the .pth approach sufficient on its own.
Payload obfuscation
The JavaScript payload is wrapped in three layers:
- A ROT-N character cipher applied to an integer array (the rotation value varies per package: ROT-13 for [email protected], ROT-17 for rsquests, ROT-25 for tlask)
- AES-128-GCM encryption with hardcoded keys, producing two encrypted blobs
- Standard variable-name mangling (_0x prefix obfuscation) on the inner payload
We decrypted the payload through static analysis without executing any code. The first blob (907 bytes) is the Bun runtime downloader. The second blob (772 KB) is the complete Shai-Hulud credential stealer, containing 2,538 hardcoded strings.
For researchers performing their own analysis, here are the AES decryption keys:
| Bun downloader | c95506221d18936328fbc7ddcd21e3dd | 48da5faeafac0ac88a410bb0 |
| Worm payload | 7557c4e782a0622159476d1ea10d5236 | 55a7d25e0e61b77cc175bcc3 |
Credential harvesting
Once running, the worm goes after credentials across every major cloud and CI/CD platform:
- GitHub Actions: GITHUB_TOKEN, personal access tokens, fine-grained tokens, OIDC tokens, organization and repository secrets, Actions artifacts, and runner process memory
- AWS: IAM access keys, secret keys, session tokens, IMDS instance credentials (169[.]254[.]169[.]254), Secrets Manager entries, SSM parameters, STS federation tokens
- Azure: Client secrets, managed identity tokens, Key Vault secrets, federated credentials, Microsoft Graph API tokens
- GCP: Service account keys, application default credentials, cloud-platform scope tokens
- HashiCorp Vault: Vault tokens from seven known filesystem paths (/var/run/secrets/vault-token, /etc/vault/token, /root/.vault-token, and others), plus API access and Kubernetes Vault auth
- npm / JFrog: npm tokens, JFrog/Artifactory API keys, OIDC token exchange
- PyPI: Publishing tokens, OIDC mint tokens
- RubyGems: API keys, gem publishing credentials
- SSH: Private keys for lateral movement
- Kubernetes: Service account tokens, kubeconfig files
- Sigstore: OIDC tokens and Fulcio signing certificates, which would allow the attacker to sign artifacts under a trusted identity
- Databases: MongoDB, MySQL, PostgreSQL, and Redis connection strings with embedded passwords
Self-propagation
Like the original npm variant, this is not just a stealer. It propagates. Using stolen credentials, the worm:
- Commits .github/setup.js and workflow files to accessible GitHub repositories, causing the worm to re-execute in other CI pipelines
- Injects .github/copilot-instructions.md to poison AI code assistants
- Publishes additional poisoned packages to PyPI, npm, and RubyGems using stolen registry tokens
- Attempts privilege escalation on self-hosted CI runners by injecting sudoers rules
- Checks for StepSecurity's harden-runner and adjusts behavior if detected
The attacker
All five packages are owned by the PyPI account elitexp. The account was created in November 2024 with a legitimate package (mflux-streamlit, a Streamlit UI for image generation with 11 stars on GitHub). The associated GitHub account (github[.]com/elitexp) is 13+ years old with 43 public repositories, including university coursework and Laravel projects.
Upload metadata shows all packages were published using Bun/1.3.14 as the user-agent, the same runtime the malware downloads as part of its execution chain.
The attacker also weaponized mflux-streamlit itself. Versions 0.0.1 and 0.0.2 are clean, but Versions 0.0.3 and 0.0.4, published at 15:23 and 15:37 UTC after the typosquat campaign, contain the same .pth dropper and obfuscated payload. This makes the attack more dangerous than a typical typosquat: mflux-streamlit is a real project with existing users who may receive the poisoned update through normal dependency resolution.
Indicators of compromise
| package | rlask 3.1.4-3.1.7 | Malicious Flask typosquat |
| package | tlask 3.1.4 | Malicious Flask typosquat |
| package | rsquests 2.34.3 | Malicious Requests typosquat |
| package | nhmpy 2.4.7 | Malicious NumPy typosquat |
| package | mflux-streamlit 0.0.3, 0.0.4 | Weaponized legitimate package |
| file | {package}-setup.pth | Auto-executing dropper (SHA256: 6506d317...) |
| file | sitecustomize.py | Backup auto-execution (present in rlask only) |
| file | {package}/_index.js | Obfuscated worm payload (5.2MB) |
| file | .bun_ran | Execution marker in system temp directory |
| network | hxxps[://]github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/bun-{os}-{arch}.zip | Bun runtime download |
| network | hxxps[://]upload[.]pypi[.]org/legacy/ | Worm publishes poisoned PyPI packages |
| network | hxxp[://]169[.]254[.]169[.]254/latest/meta-data/iam/security-credentials/ | AWS IMDS credential theft |
| network | hxxps[://]login[.]microsoftonline[.]com/ | Azure AD token acquisition |
| network | hxxps[://]fulcio[.]sigstore[.]dev | Sigstore certificate request |
| actor | elitexp (PyPI) | Package owner |
| actor | Bun/1.3.14 | Upload user-agent |
What to do if you're affected
If any of these packages were installed in your environment:
- Remove the package immediately and check for the .bun_ran marker file in your system temp directory.
- Rotate all credentials that were accessible to the environment where the package was installed. This includes CI/CD tokens, cloud provider credentials, SSH keys, and registry publishing tokens.
- Audit your GitHub repositories for unexpected commits, especially files matching .github/setup.js, .github/copilot-instructions.md, or modified workflow files.
- Check your package registry accounts (PyPI, npm, RubyGems) for packages you did not publish.
- Review CI/CD pipeline logs for unexpected Bun downloads or JavaScript execution.
Timeline
| 2026-05-12 | TeamPCP open-sources the Shai-Hulud worm |
| 2026-06-07 13:47 UTC | Probe versions published ([email protected], [email protected]) |
| 2026-06-07 14:20 UTC | First malicious version ([email protected]), detected within 28 seconds |
| 2026-06-07 14:24 UTC | Automated analysis complete, flagged as malicious/critical |
| 2026-06-07 14:27-15:04 UTC | Six more malicious versions published across all four package names |
| 2026-06-07 15:23-15:37 UTC | Attacker weaponizes their own legitimate mflux-streamlit package (v0.0.3, v0.0.4) |
| 2026-06-07 | Investigation confirms full Shai-Hulud worm via static analysis |
| 2026-06-07 16:01 UTC | All malicious packages reported to PyPI security team |
| 2026-06-08 03:15:06 UTC | Added the Advisory to GitLab Advisory Database |
| 2026-06-08 | PyPI removed all releases of the malicious packages |
How GitLab can help you detect these packages
If you are using GitLab Ultimate, you can use Dependency Scanning to automatically surface exposure to these packages in your projects. We have filed advisories (GMS-2026-572 through GMS-2026-576) covering all five packages in the GitLab Advisory Database. Once merged, any project with Dependency Scanning enabled will flag these packages in pipeline results and the Vulnerability Report.
For teams managing many repositories, GitLab Duo Chat with the Security Analyst Agent can help triage quickly. Ask questions like:
- "Are any of my dependencies affected by the Shai-Hulud PyPI campaign?"
- "Does this project have any malicious Python dependencies?"
Looking ahead
We expected this campaign after TeamPCP open-sourced the Shai-Hulud worm in May. Independent actors are picking up the toolkit and deploying it against new ecosystems. The Python variant uses a different initial infection vector (.pth files instead of preinstall scripts) but carries the same credential harvesting and self-propagation code underneath.
Our monitoring systems continue to track copycat deployments across npm, PyPI, and other registries. We will update this post as more information becomes available.
Find more articles from the Vulnerability Research team on our Security Labs site.