Guide · Backend
npm October 2026 SlugFest Flaw Explained: How Reclaimed Repo Names Could Publish
20 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The npm trusted publishing vulnerability, which the security company Bloom Security named SlugFest, let whoever claimed a freed GitHub owner/repo name publish through a never-used trust rule.
- Trusted publishing lets a CI job, an automated build server, publish with a short-lived signed token instead of a long-lived npm token kept as a secret. Before a rule's first publish, npm checked names, not GitHub's permanent repository ID.
- Bloom reported the flaw on 6 June 2026 and counted more than 1,500 exposed packages. GitHub's changelog announced a change to npm on 2 October, and Bloom published the details on 13 October.
- Since 2 October, a rule that has not published within 48 hours expires, and tokens from comment-triggered workflow runs are refused. WorkOS, a company that builds sign-in and user-management tools for developers, argues this narrows the gap rather than closing it.
- Beginners can stop after the section on names and IDs. Engineers then get what npm trust list does not show, a rename trap in naive name checks, and how to read the repository ID inside provenance.
What goes wrong when npm trusts a repository name instead of its ID?
Until 2 October 2026, if an npm trust rule had never been used, whoever owned the GitHub name it pointed at could publish your package. GitHub names can change hands: when an organisation is renamed, someone else can register the old name.
That is the npm trusted publishing vulnerability that Bloom Security disclosed on 13 October 2026 and named SlugFest. A slug is the short owner/repo name of a GitHub repository, and npm treated it like an identity.
Picture a common setup. A small team maintains an open-source npm package that draws a UPI payment button on a few hundred shop websites. UPI is India's instant bank-to-bank payment system. The setup is made up, but the flaw is real.
Those shops take about 2 crore rupees (20 million rupees) a day through the button. In May, a maintainer added a trusted publisher on npm for the repository OLDORG/pay-widget and the workflow file release.yml. The first automated release kept slipping, so that rule never published anything.
In August, the team renamed its GitHub organisation from OLDORG to NEWORG. Nobody updated the npm rule. When the SlugFest write-up appears, the team asks one question: who can own the name OLDORG today?
Which words do you need before reading about SlugFest?
You need five words: OIDC token, trusted publisher, slug, repository ID and provenance.
- OIDC token. OpenID Connect, or OIDC, is a standard way for one system to vouch for another. GitHub Actions, GitHub's built-in CI service, can give a running job a signed, short-lived token that names its repository and workflow.
- Trusted publisher. A rule on an npm package that says: accept releases from this repository and this workflow file. Bloom calls it a binding and GitHub calls it a configuration. This guide says binding.
- Slug. The readable owner/repo name, such as OLDORG/pay-widget. Owners can change it, and someone else can take a freed name.
- Repository ID. A number GitHub gives every repository, carried in the token as repository_id. It stays the same through renames. A new repository created at an old name gets a new number.
- Provenance. A signed record of where and how a package was built. npm creates it with Sigstore, a public signing service that keeps a public log.
What is the npm trusted publishing vulnerability called SlugFest?
It is a gap in how npm decided who may publish. Until a binding had published once, npm compared names with the binding: the owner/repo name and the workflow file name in the token. According to Bloom, it recorded the permanent repository IDs only after that first publish, which npm calls validation.
Bloom Security reported the flaw on 6 June 2026 through GitHub's HackerOne programme, a service where outside researchers report bugs. GitHub's changelog describes the change without naming the flaw, so the mechanism below comes from Bloom.
Bloom scanned the registry and found more than 1,500 exposed packages, with over 10 million total downloads. It has not published the list, saying the issue is still prevalent.
| Date | What happened |
|---|---|
| 31 July 2025 | npm trusted publishing with OIDC becomes generally available |
| 6 June 2026 | Bloom reports the flaw through GitHub's HackerOne programme |
| 9 June 2026 | The report is triaged and validated |
| 8 July 2026 | Last reply before the fix; six follow-ups, one a HackerOne mediation request, go unanswered until npm confirms the fix |
| 2 October 2026 | GitHub changelog: unvalidated configurations now expire after 48 hours |
| 13 October 2026 | Bloom publishes the SlugFest write-up |
| 16 October 2026 | WorkOS publishes its analysis of the fix |
How does npm trusted publishing with OIDC work?
Your CI workflow, the job that builds and releases your code, proves who it is with a short-lived OIDC token. The npm registry, the server that stores packages, checks the token against the binding. Then it swaps the token for a short-lived publish credential.
No long-lived npm token sits in your repository secrets. To set it up, you add the repository and workflow file name on npm, and give the workflow the id-token: write permission. That permission lets the job ask GitHub for an OIDC token.
One trusted publishing release, step by step
Drag to exploreBoth tokens are short-lived. Notice the binding check in the middle: it decides who gets a publish credential, and it is where SlugFest struck.
Diagram connections:
- release.yml runs to Signed OIDC token (id-token: write)
- Signed OIDC token to Token exchange request
- Token exchange request to Binding check
- Binding check to Short-lived publish token (Claims match)
- Short-lived publish token to npm publish
- release.yml runs: Workflow → GitHub: please issue an OIDC token
- Signed OIDC token: GitHub → workflow: repo name, IDs, file, event
- Token exchange request: Workflow → npm registry: here is my OIDC token
- Binding check: npm registry: compare token claims with the binding
- Short-lived publish token: npm registry → workflow: short-lived credential
- npm publish: Workflow → npm registry: package plus provenance
The token carries claims, which are named facts about the run. GitHub documents claims such as repository, workflow and event_name, plus the numeric repository_id and repository_owner_id.
Publishing through trusted publishing also creates provenance automatically. npm's docs scope this to public packages published from public repositories.
Why is a repository name weaker than a repository ID?
A name is a label that people can change or give up. The ID stays with the repository, so only the ID proves it is still the same repository.
Think of a shop sign and the shop owner's PAN, the permanent tax number every Indian taxpayer gets. The sign is the slug: whoever takes over the premises can put up the same sign. The PAN is the repository ID: a newcomer gets a different one.
Where the picture breaks: a PAN names a person, while repository_id names one repository. A transferred repository keeps its repository_id, so WorkOS advises checking repository_owner_id as well to catch transfers.
| Value in the token | Example from the setup | Who can change it | Safe to trust alone? |
|---|---|---|---|
| Owner/repo name (slug) | OLDORG/pay-widget | The owner, or whoever registers a freed owner name | No |
| Workflow file name | release.yml | Whoever controls a repository at that name | No |
| repository_id | 812345678 (made up) | Nobody; it survives renames | Yes, if stored when the binding is made |
| repository_owner_id | A fixed number for the organisation | Nobody for the organisation; a transfer moves the repository to another owner ID | No; it names the organisation. Add it to repository_id to catch transfers |
Who can claim a freed name depends on what changed. Bloom notes that renaming or deleting the owner account, an organisation or a username, frees the owner name for anyone to register. GitHub's docs say the same for a renamed organisation.
A repository renamed, moved or deleted inside an account you still control frees the name only to people who can create repositories there.
GitHub does retire some old names for good. After an organisation rename, GitHub permanently retires the old owner/repo name of some public repositories. That covers a repository with over 100 clones or 100 Actions uses in the week before, or with a Marketplace action.
WorkOS notes that this retirement rule narrows the exposed set somewhat. Repositories below those thresholds, like many small packages, get no such protection.
Bloom notes that PyPI's Warehouse, the software behind Python's package registry, already avoids this. It looks up the IDs for a name when the trust rule is set up, then stores them.
npm stored the IDs too, but only after the first successful publish. Before that, the team's binding was just a name and a file name. Anyone holding both could pass the check.
How could an attacker publish through a reclaimed repository name?
By re-creating the old name and running a workflow with the expected file name. Bloom describes the steps, and none of them needs a stolen password or token.
The SlugFest path through a stale binding
Drag to exploreEach step is ordinary GitHub use. The outcome depends only on whether the binding has ever published.
Diagram connections:
- Organisation renamed to Old name is free
- Old name is free to Attacker claims the name
- Attacker claims the name to Adds release.yml
- Adds release.yml to OIDC token issued
- OIDC token issued to Binding ever published?
- Binding ever published? to Malicious version live (No: names compared)
- Binding ever published? to Exchange refused (Yes: IDs compared)
- Organisation renamed: OLDORG becomes NEWORG
- Old name is free: Binding still says OLDORG/pay-widget
- Attacker claims the name: New repo at the old name gets a new ID
- Adds release.yml: Same file name, with id-token: write
- OIDC token issued: Name and file match; repository ID does not
- Binding ever published?: Decides which check npm runs
- Malicious version live: Signed provenance pointing at the attacker's repo
- Exchange refused: Stored ID differs from the token's ID
The precondition is narrow. The binding must point at a name that is now free, and it must never have published a version. The team's binding fits both.
What a binding stores, seen from the npm CLI
You can see the weak key yourself, with no npm account. A dry run of npm trust in npm 11.21.1 prints what it would ask npm to store and stops before any request. This is the output we got:
$ npm trust github demo-pkg --repo OLDORG/pay-widget --file release.yml --allow-publish --dry-run --json
{
"package": "demo-pkg",
"file": "release.yml",
"repository": "OLDORG/pay-widget",
"permissions": [
"createPackage"
]
}What just happened? The request holds a package name, a workflow file name and an owner/repo string, with no repository ID. createPackage is the registry's name for the --allow-publish permission.
Bloom reports that the registry adds the IDs only after the first successful publish. Without --json, the same command warns that anyone with write access to that GitHub repository can publish the package.
Bloom adds that AI coding agents now pick and install dependencies on their own, often without a human checking. A hijacked release could reach many machines that way.
What did GitHub change for npm trusted publishing on 2 October 2026?
GitHub made unvalidated npm trusted publishing configurations expire 48 hours after creation. A configuration becomes validated, and exempt from expiry, after its first successful publish.
- Expiry. A binding that has not published within 48 hours can no longer authorise publishing. Expired bindings stay visible in the package's trusted publisher settings, and you recreate one to start a fresh window.
- Identity changes. Pointing a binding at a different repository or project needs a new trust relationship, with its own 48-hour window. Ordinary edits do not restart the clock.
- Event block. npm now rejects tokens from GitHub Actions issue_comment events, which run when someone comments on an issue or pull request. Tokens from pull_request_target events were already rejected.
A pull_request_target run uses the main repository's context even for pull requests from forks, which is why GitHub's docs warn against running untrusted code there.
npm's documentation states the same deadline as 2 days and adds the key detail. The first successful publish binds the configuration to the repository's immutable identity. Bloom says that is when the numeric IDs get stored.
Is the 48-hour expiry enough to stop SlugFest?
Pause and predict: the team's binding was created in May and never used. Did the 2 October change retire it? Hold that question for two paragraphs.
The expiry shrinks the risk but keeps the same weak key, according to WorkOS's analysis. A new binding still starts from a name, so for up to 48 hours it trusts whoever holds that name.
WorkOS also notes that the changelog does not say what happened to bindings that were already unvalidated on 2 October. That is exactly the team's case, and as of 19 October 2026 the changelog still does not say.
| Situation | Before 2 October | After 2 October | IDs pinned at creation |
|---|---|---|---|
| Binding never used, name freed months later | Attacker can publish | Binding expires after 48 hours | Attacker blocked |
| Name claimed within 48 hours of creating the binding | Attacker can publish | Attacker can still publish | Attacker blocked |
| Binding already unvalidated on 2 October | Exposed | Not addressed in the changelog | Attacker blocked |
| Binding has published once | IDs stored, attacker blocked | IDs stored, attacker blocked | Attacker blocked |
For the team, the expiry may already have retired the May binding. Nobody wants to bet 2 crore rupees of daily payments on a maybe, so the team checks.
A related change on GitHub's side
Since 15 July 2026, GitHub's OIDC subject claim has carried the owner ID and repository ID for repositories created after that date. Repositories renamed or transferred after it switch too, and older ones can opt in.
Neither GitHub's npm changelog nor npm's docs say npm uses this claim, as of 19 October 2026. GitHub's docs also say the change does not reach GitHub Enterprise Server.
Does Sigstore provenance prove an npm package is safe?
No. Provenance proves where a package was built, not that it was built where you expected. npm's provenance documentation says it does not guarantee the package has no malicious code.
Bloom notes that the attacker's release would be signed by Sigstore against the attacker's repository. npm audit signatures checks that registry signatures and attestations, the signed provenance records, are valid. In npm 11.21.1 it does not compare the repository with one you expected.
Read the repository ID from the provenance certificate
The registry serves each version's attestations as JSON. Each provenance record is signed with a short-lived certificate from Fulcio, Sigstore's certificate authority. This script prints the repository fields from that certificate for semver 7.7.3.
import base64
import json
import urllib.request
from cryptography import x509 # pip install cryptography
PACKAGE, VERSION = "semver", "7.7.3"
FIELDS = {"1.3.6.1.4.1.57264.1.12": "repository",
"1.3.6.1.4.1.57264.1.15": "repository_id",
"1.3.6.1.4.1.57264.1.17": "repository_owner_id"}
def der_text(raw):
# DER string: tag byte, then a short or long-form length, then text
size, start = raw[1], 2
if size & 0x80:
count = size & 0x7F
size = int.from_bytes(raw[2:2 + count], "big")
start = 2 + count
return raw[start:start + size].decode()
url = ("https://registry.npmjs.org/-/npm/v1/attestations/"
+ PACKAGE + "@" + VERSION)
with urllib.request.urlopen(url) as resp:
attestations = json.load(resp)["attestations"]
for att in attestations:
if not att["predicateType"].startswith("https://slsa.dev/provenance/"):
continue
vm = att["bundle"]["verificationMaterial"]
cert = vm.get("certificate") or vm["x509CertificateChain"]["certificates"][0]
cert = x509.load_der_x509_certificate(base64.b64decode(cert["rawBytes"]))
for ext in cert.extensions:
name = FIELDS.get(ext.oid.dotted_string)
if name:
print(name + ":", der_text(ext.value.value))We ran it on 19 October 2026 and got this output:
repository: https://github.com/npm/node-semver
repository_id: 1357199
repository_owner_id: 6078720Here is what those lines mean. Sigstore's Fulcio documentation maps these certificate fields to GitHub's repository_id and repository_owner_id token claims. A release built at a reclaimed name would carry the new repository's ID.
Compare it with your real repository's ID: the id GitHub's API returns for NEWORG/pay-widget, or an earlier release that has provenance. Certificates from early 2023, such as sigstore 1.0.0's, lack these fields, so the script prints nothing for them.
The same IDs also appear inside the provenance statement, but npm 11.21.1 copies those from environment variables during the build. Prefer the certificate. The script only reads it; npm audit signatures does the verifying.
What should npm maintainers do after SlugFest?
Check every binding, recreate any that point at old names, and publish once through each new one. These steps follow Bloom's and WorkOS's advice.
Check your npm version
Run npm --version. npm's docs ask for npm 11.15.0 or later for npm trust, as of 19 October 2026. On npm 10 the command prints: Unknown command: "trust".
List your bindings
Run npm trust list PACKAGE for each package. It prints each binding's id, type, workflow file and repository.
Confirm you own every name
Check that each owner/repo name is still a repository you control, under the same name.
Revoke and recreate stale bindings
Run npm trust revoke PACKAGE --id=TRUST_ID with the id from the list. Then create a binding for the current name with npm trust github and --allow-publish.
Publish straight away
Release once through the new binding so it validates and stores the repository's immutable identity.
Treat renames as revocation
Remove the binding before you rename, transfer or delete a bound repository, not after the old name is freed.
Watch for surprise releases
Alert on any publish you did not start.
What npm trust list does not show
The npm trust command first shipped in npm 11.10.0, per GitHub's February 2026 changelog. Version 11.15.0 added the --allow-publish and --allow-stage-publish flags, and from that version the CLI refuses to create a binding without one. The docs do not say why they ask for 11.15.0.
We read the command's source in npm 11.21.1 and 12.2.0. npm trust list prints id, type, file, repository, environment and permissions. It does not print whether a binding is validated or expired. GitHub's changelog says expired bindings stay visible in the package's trusted publisher settings on npmjs.com; we could not check how that page labels them.
A name check that misses renames
The naive check asks GitHub's API for the ID behind each name and compares it with the ID you noted. That is wrong for renames. GitHub's API still redirects requests for a renamed or moved repository's old name, so the old name returns your ID. GitHub's rename docs say API requests that use the old organisation's name return 404. In practice, repository requests are still redirected, as the next example shows.
Compare the name GitHub reports as well. This script does both, using GitHub's public API without a token.
import json
import urllib.error
import urllib.request
API = "https://api.github.com/repos/"
# Each binding's owner/repo name and the repository ID you noted
# when you created it (made-up values)
expected = {"OLDORG/pay-widget": 812345678}
def lookup(slug):
try:
with urllib.request.urlopen(API + slug) as resp:
data = json.load(resp)
except urllib.error.HTTPError as err:
return None, "HTTP " + str(err.code)
return data["id"], data["full_name"]
for slug, want in expected.items():
got_id, name = lookup(slug)
if got_id != want:
print(slug, "DANGER: not your repository now:", got_id, name)
elif name.lower() != slug.lower():
print(slug, "STALE: GitHub redirects this name to", name)
else:
print(slug, "OK")For the team's stale binding, the script prints: OLDORG/pay-widget STALE: GitHub redirects this name to NEWORG/pay-widget. A real case, as of 19 October 2026: GitHub's API answers the old name zeit/next.js with vercel/next.js, ID 70107786.
A new organisation with a different ID now holds the name zeit, yet the redirect still works because no zeit/next.js repository exists. The script would print STALE there, while an ID-only check expecting 70107786 would print OK.
A reclaimed name prints DANGER with a different ID. HTTP 404 means deleted or private, because GitHub answers 404 for a private repository when the request has no token. A deleted owner frees the name to anyone, so treat a 404 as stale until you know why.
HTTP 403 or 429 means you hit GitHub's rate limit, not a takeover. Wait, or send the request with a GitHub token.
The team's npm trust list run shows the old OLDORG binding. They revoke it, create one for NEWORG/pay-widget and ship version 2.4.1 through it that afternoon. Nobody had claimed the old name, so this ends as a cleanup.
How do you explain the npm trusted publishing vulnerability in a system design interview?
Frame it as an authorisation bug, a mistake in deciding who may act. The system checked a name that can change hands, not an ID that cannot.
Describe the trust flow
CI gets an OIDC token, npm checks it against a stored binding, then swaps it for a short-lived publish credential.
Point at the weak key
Before the first publish, the binding matched only names, and names can be freed and claimed.
Walk through the attack
Claim the freed name, add a workflow with the same file name, get a matching token and publish.
Compare the fixes
GitHub now expires unused bindings after 48 hours. Bloom's proposal, pinning the repository ID at creation, would close that window too, if the name is still yours at creation.
Generalise the lesson
Usernames, email addresses and domains can be reclaimed too. Key authorisation on IDs the system assigns and never reuses.
Questions people ask
- Was my npm package affected by SlugFest?
- Only if it had a binding that had never published and named a repository someone else could claim. Since 2 October, such a binding also expires 48 hours after creation. Bindings that had published once already had IDs stored, according to Bloom. Run npm trust list and confirm every listed repository is still yours.
- Is my package safe if its binding has already published once?
- It is not exposed to SlugFest's name-only check. After the first publish, npm stores the repository's numeric IDs, according to Bloom. Still recreate the binding after any rename or transfer, and remove it before deleting a repository, as Bloom and WorkOS advise.
- Was the SlugFest flaw exploited in the wild?
- No malicious publish had been publicly confirmed as of 19 October 2026. Bloom does not report one, and WorkOS calls exploitation unknown. Bloom tested the attack only with an npm scope, or package namespace, and GitHub repositories that it controlled.
- Should I stop using npm trusted publishing?
- No. Bloom writes that the architecture is correct and the omission is fixable. Trusted publishing removes long-lived automation tokens, which can leak. Publish once right after creating a binding and recreate bindings after renames. npm's docs also recommend disallowing token-based publishing once trusted publishing works.
- Does SlugFest affect GitLab trusted publishing on npm?
- Bloom says the same issue existed in npm's GitLab trusted publishing integration. GitLab bindings name a namespace, meaning a user or group, and a project name, which can also change. Run npm trust list, confirm each path is still yours and recreate the binding after any move.
- How is SlugFest different from the TanStack npm hack?
- In the TanStack compromise of 11 May 2026, the attacker ran code inside TanStack's own GitHub Actions setup through a fork pull request and a poisoned cache. SlugFest needs no access to the real pipeline. The attacker builds a fresh repository at a freed name and passes npm's name check.
Sources and further reading
- GitHub Changelog, 2 October 2026: Unvalidated npm trusted publishing configurations now expire
- Bloom Security: SlugFest disclosure (13 October 2026)
- WorkOS: SlugFest analysis (16 October 2026)
- npm Docs: Trusted publishing for npm packages
- npm Docs: npm trust (npm v11 CLI)
- npm Docs: Generating provenance statements
- GitHub Docs: OIDC token claims and immutable subject claims
- GitHub Docs: Renaming an organisation (claimable names and retirement)
- GitHub Changelog, 31 July 2025: npm trusted publishing with OIDC is generally available
- GitHub Changelog, 18 February 2026: npm bulk trusted publishing config (npm trust in CLI 11.10.0)
- GitHub Developer: Repository redirects in the REST API (2015)
- GitHub Docs: Events that trigger workflows (issue_comment, pull_request_target)
- GitHub Docs: Troubleshooting the REST API (404 for private resources, 403 or 429 rate limits)
- Sigstore Fulcio: certificate extension OIDs and their GitHub claim mapping
- TanStack: npm supply chain compromise postmortem (11 May 2026 incident)
Related guides
- TanStack npm Hack Explained: What JavaScript Developers Must Do NowBackend · 18 min read
- GitHub's November 2026 Pull Request Target Block Explained: How to Fix WorkflowsBackend · 20 min read
- Axios Supply Chain Attack 2026: Malicious npm Versions, Postinstall Malware, and How to Protect Your SystemsBackend · 16 min read