← Back to blog

Mini Shai-Hulud and deny.sh

12 May 2026 · 4 min read
Verdict

deny.sh is not affected by the Mini Shai-Hulud supply-chain attack of 11 May 2026. No compromised package is present in any of our lockfiles, direct or transitive. The attack's exploit path is not available against any of our publishing surfaces. Specifics below.

What happened

On 11 May 2026, between roughly 19:20 and 19:26 UTC, an attacker calling themselves TeamPCP published 84 malicious versions across 42 @tanstack/* npm packages, along with several packages in @squawk/* and @mistralai/*. The campaign has been named "Mini Shai-Hulud" by Wiz, Snyk, and StepSecurity, after the larger Shai-Hulud worm that hit the registry in late 2025.

The full postmortem from TanStack is the canonical primary source. The exploit chain, in plain terms, was:

  1. A fresh GitHub account forked the TanStack/router repository.
  2. The fork submitted a pull request that abused the pull_request_target trigger in TanStack's GitHub Actions workflow. This is a known footgun that runs CI with the BASE repo's secrets in scope, even when the PR comes from a fork.
  3. The malicious workflow stole a GitHub Actions cache and an OIDC token, then used them to publish to npm under TanStack's own publishing identity, producing tarballs with valid SLSA provenance.
  4. The published payload was credential-stealing code targeting CI systems and developer machines that installed the affected versions.

This is a well-known attack class. The vulnerable pattern is well documented by GitHub itself in their "Preventing Pwn Requests" post from 2020. It does not need to be there. It is a configuration choice.

Why deny.sh is not affected

1. None of our code consumes the compromised packages

None of the compromised namespaces appear anywhere in our dependency trees, direct or transitive. The deny.sh public site is static HTML and a small amount of vanilla JavaScript. The SDKs ship with zero runtime dependencies, a constraint we hold deliberately. The MCP server has a small, hand-picked dependency tree, none of which touches @tanstack/*, @squawk/*, or @mistralai/*. We know what is in the tree because we keep it small enough to know.

2. The exploit path does not exist against our publishing surface

The attack required a workflow with the pull_request_target trigger. We have searched every .github/workflows/ directory across every deny.sh repo. There is no pull_request_target trigger anywhere. Forks cannot fire any of our publishing workflows.

Every npm, cargo, and Go release has been published by hand, from a single hardened machine. There is no CI in the npm publishing path. The PyPI workflow exists for tag-triggered builds, but it runs on push: tags only, with read-only repo permissions, and uses PyPI's OIDC trusted-publishing mechanism, which holds no static token for an attacker to steal.

This is a deliberate choice, not a process gap waiting to be automated. Automating a release path trades a small amount of human time for a permanent attack surface inside your CI provider. For a deniability product, that is a bad trade. We type the version number, read what is in the tarball, sign the git tag, and walk away. That is how every release of deny.sh has shipped since 1.0.0, and how every release will ship until we have a reason stronger than convenience to change it.

3. Our publishing tokens are scoped and recoverable

The npm tokens used to publish deny-sh, deny-sh-mcp, and related packages are npm Granular Access Tokens, scoped to specific package names, with the publishing IP allow-list pinned to the droplet that we publish from. They cannot be used from any other machine on the internet, even if they leaked. We have already tested the leaked-token revocation path once in production (26 April 2026, a separate, contained incident on the crates.io flow) and it works in under three minutes from detection to revoke.

4. Everything is lockfile-pinned

Every package.json in our trees pins exact versions, no caret or tilde ranges, after a sweep we ran during the launch-prep audit. Every lockfile is committed to git. A fresh npm ci in any of our repos installs the exact versions that were resolved when the lockfile was authored, not whatever happens to be the latest at install time. This is what stopped Mini Shai-Hulud from quietly entering build environments at thousands of consumers, including ours, in the window before npm un-published the malicious versions.

What this incident teaches us about provenance

The most interesting part of Mini Shai-Hulud is that the malicious tarballs shipped with valid SLSA provenance. The attacker did not bypass SLSA. They captured it. Because they took over the project's own CI/CD identity, the provenance machinery dutifully signed the malicious artifacts and said "yes, these came from the expected pipeline."

SLSA provenance answers the question "did this artifact come from the expected build pipeline?" It does not answer the question "is the pipeline itself trustworthy?"

This matters for how we tell our trust story. The honest claim for deny.sh today is not "we have SLSA provenance via CI." It is "we publish by hand, from one machine, with scoped tokens, and the git tag for each release is signed with a key whose fingerprint is published." Today, that is a stronger trust signal than a hijackable CI pipeline.

We will revisit this stance over time. SLSA provenance plus hardened workflows (pinned action SHAs, no pull_request_target, isolated cache scopes, ephemeral OIDC) is a defensible posture once those hardening steps are in place. We are not there today, and we are honest about it.

What we will do as a result

If you have a question

If you are using deny.sh in a build or a product and need a written confirmation for your own security review, email security@deny.sh. The PGP fingerprint is on /disclosure. The supply-chain verification recipe is in our verification walkthrough.

Related: How to verify deny.sh · Cryptographic construction · Security Posture · Coordinated disclosure