zod, nub and standard-schema: A Push Credential Reaches Whatever a Push Can Start

Written by the Rafter Team

As of September 22, 2026.
Whoever can push to your repository can publish your package, unless the step that publishes needs something a push cannot supply. On September 21 three JavaScript projects got the same malicious commits from what appears to be one stolen push credential. Whether each one reached npm came down to what its release needed besides a push.
What happened
GitHub's incident write-up, posted on September 21, describes malicious commits in three public repositories: colinhacks/zod, standard-schema/standard-schema and nubjs/nub.
All three initial commits used the message Fix bugs and improve performance, and each added the same loader through a package install hook.
The loader queried a fixed Ethereum smart contract for a delivery address, downloaded JavaScript from it, ran that code with the permissions of the install process, and deleted its temporary files.
Because the payload came from a server, it could change without another package release.
When GitHub's investigators fetched it, the response only printed game over, which says nothing about what it served earlier.
| Repository | Commit time (UTC) | npm outcome |
|---|---|---|
| zod | 11:56 | Did not reach npm publishing |
| standard-schema | 12:37 | Did not reach npm publishing |
| nub | 12:54, then 13:00 | @nubjs/nub@0.9.4 submitted, blocked at 13:09 |
npm's publish-time malware scanning, introduced in July, held @nubjs/nub@0.9.4 in validation, rated it High risk, and blocked it before anyone could install it.
GitHub says the evidence does not show that any downstream system ran the malicious code.
The cause: a push credential, not a publish token
Zod's maintainer, Colin McDonnell, pushed back on parts of the write-up early the next day. "Seems like a git push credential of some kind was compromised, not an npm or GitHub token," he wrote, adding that the attack "consisted of malicious commits and tagged pushes to main."
His account of the three outcomes:
- nub: "The Nub release CI WAS triggerable via tag creation." A tagged push started a release, so the push credential was enough to reach npm, and the malware scan was the last control left. He called that catch "a BIG win."
- standard-schema: not exposed to this kind of attack "since npm removed token-based publishing flows last December."
- zod: "Zod's release flow was already secured against [this] kind of attack via trusted publishing and a dedicated GitHub environment for secure deployments."
The bad commits went after the gate
The cleanup is more telling than the write-up. PR #6620, titled "Cleanup bad commits" and merged at 21:33 UTC on September 21, reverts six commits. Besides the two loader commits, it reverts:
ci: drop the npm environment approval gate from publish, which deleted theenvironment: npmline from zod's release workflow;- two version bumps, to
4.6.7and4.6.8; ci: publish to npm with NPM_TOKEN, a new workflow that tried to publish on every push to main with a token secret.
So whoever pushed those commits saw the gate and tried to take it apart.
Neither attempt shipped anything.
The release run for the 4.6.8 bump was cancelled before it published, and the token workflow's publish step failed when npm answered 404 Not Found.
npm's version history shows no zod release after 4.6.5 on September 13.
The reason is in zod's own workflow file, in the comment above the line the attacker deleted: a required reviewer approves every publish, the environment only deploys main, and "npm's trusted publishers are bound to it so no other branch or workflow can publish." Two pieces of that gate live outside the repository. The required reviewer is a GitHub environment setting, and the binding to that environment is part of zod's trusted publisher configuration on npm. Deleting a line from a YAML file changes neither.
npm refused the token workflow's publish. A project that publishes through trusted publishing has no need to keep a long-lived npm token in its secrets, which leaves a workflow like that one with nothing to authenticate with.
Why this generalizes
Most release hardening lives in the repository: a workflow that runs tests first, a job that only runs on tags, a branch filter. All of it is text that anyone with push access can edit. A control that a push can rewrite does not protect against a stolen push credential.
The question to ask of any release pipeline is what a push alone can start. For nub on September 21, the answer was a publish. For zod, the answer was a workflow run that stopped at a gate configured somewhere the push could not reach.
Install hooks are the other half of the story. All three payloads ran through a lifecycle script, the same bug class that carried the Shai-Hulud worms. npm's scan stopped this one at the registry, but GitHub notes that blocking a version does not stop someone from fetching and running the malicious source from elsewhere.
What to do now
- List every event that can start a publish in each repository you release from: tags, pushes to main, changes to
package.json, manual dispatch. Anyone who can cause one of those can publish your package. - Move to trusted publishing and delete long-lived npm tokens. Search repository and organization secrets for
NPM_TOKENand similar, and revoke them on npm once the OIDC path works. - Bind the trusted publisher to a GitHub environment with required reviewers and a deployment branch rule limited to main. Those settings live outside the repository, so a push cannot remove them.
- Protect main and tags with rulesets. Require pull requests for main, restrict who can create release tags, and consider requiring signed commits so a stolen push credential is not enough on its own.
- Require review for
.github/workflows/. ACODEOWNERSentry with required review makes "drop the approval gate" a pull request someone has to approve. - Check for exposure if you built from source. If you installed zod, standard-schema or nub from a git URL, or ran their code from a checkout of main on September 21, check for the loader files named in GitHub's write-up and for connections to
dsakldjklsaj33112[.]com. - Review your own CI's install step. Running
npm ci --ignore-scriptswhere you can, and allowing install scripts only for packages that need them, removes the path this loader used.
For more on locking down the pipeline itself, see our CI/CD security guide.
What the push could not reach
The same credential produced three outcomes in three repositories on the same morning. What separated them was where the last step before npm kept its gate: in the repository, where a push could change it, or somewhere a push could not reach.