A leaver's OAuth token copied about 170 CrowdSec private repositories, and the audit log never saw it
CrowdSec has traced the copy of about 170 private repositories to an OAuth token harvested from a departing developer's laptop by the May TanStack npm compromise. The account was still in the GitHub organisation, and the clone left no trace in the organisation audit log.
By Parminder Kumar Sharma · · 19 min read

Nine minutes and four seconds
CrowdSec's incident report of 18 September 2026 gives a start and an end for the theft of its private source code: 05:52:29 UTC to 06:01:33 UTC on 22 May 2026. That is 544 seconds. Across the roughly 170 private repositories the company says were taken, it works out at about 3.2 seconds each, and the archive that surfaced on a criminal forum four months later also carried more than 130 public ones, so the real figure per repository is lower still.
The first malicious TanStack packages reached npm at 19:20:39 UTC on 11 May. The gap between that moment and the first fetch of a CrowdSec repository is 10 days, 10 hours and 31 minutes. The gap between the clone and the day CrowdSec found out about it, 16 September, is 117 days.
What those numbers do not establish is the thing most readers will assume. They do not establish that CrowdSec was compromised through its own dependency tree. CrowdSec checked its repository history against the list of backdoored versions and says that while some TanStack packages were present, none of them were on the affected list, and that its developer machines came back clean. They do not establish that infrastructure or databases were reached: CrowdSec says they were not, and that no code was altered in its open source software, its private repositories or its build pipelines.
They establish something narrower and, for anyone running a GitHub organisation, more uncomfortable. A package compromise on one person's laptop, a person who no longer worked at the company, was enough to read everything.
What the two CrowdSec statements and the GitHub advisory establish, and what they do not. Sources: CrowdSec incident report 18 September 2026; GHSA-g7cv-rxg3-hmpx.
| Question | What the sources establish | What they do not establish |
|---|---|---|
| How the packages were compromised | A chain of three known weaknesses in TanStack's GitHub Actions setup, letting an attacker mint a publish token from the release runner's memory | That any maintainer was phished or had an npm token stolen. TanStack says explicitly that none were |
| How CrowdSec was reached | An OAuth access token belonging to a developer who had just left, whose laptop was compromised by the campaign | Which application on that laptop held the token. CrowdSec does not name it |
| What the token could do | Clone about 170 private repositories in nine minutes | Any write access. CrowdSec says there were no commits and no changes to code, infrastructure or CI |
| What else leaked | One live AWS token scoped to a single SNS topic, 83 user email addresses, and 51 potential investors' names, emails and investment context | Any client login, password or log data. CrowdSec says it does not store client logs or personally identifying data |
| Who did it | CrowdSec names a forum member and attributes the package compromise to a group it calls TeamPCP, tracked as UNC6780 | We could not reach an independent primary source for either attribution. Treat both as CrowdSec's account |
The chain that put malware under a trusted identity
The upstream event is catalogued as GHSA-g7cv-rxg3-hmpx and CVE-2026-45321. Between approximately 19:20 and 19:26 UTC on 11 May 2026, 84 malicious versions across 42 @tanstack/* packages were published to npm, exactly two per package, a few minutes apart. GitHub's advisory team assigned CVSS 9.6 and rated it critical; NVD has the record as Analyzed. CISA added it to the Known Exploited Vulnerabilities catalogue on 27 May 2026 with a federal remediation date of 10 June 2026 and a ransomware flag of Known.
TanStack's own postmortem is unusually complete, and it is worth reading because the mechanism matters for the second half of this story. Three separate weaknesses were chained, and TanStack's framing is that none of them alone is sufficient:
- A
pull_request_targetworkflow, the pattern GitHub's own security team has called a pwn request for years, which checked out a fork's merge ref and ran a build with it. - GitHub Actions cache poisoning across the fork and base trust boundary. The cache save on job exit is not gated by the workflow's
permissionsblock, because it uses a runner internal token rather than the workflow token. - Extraction of the short lived OIDC publish token from the Actions runner process memory, using the same technique published after the tj-actions compromise of March 2025.
The attacker forked the router repository on 10 May at 17:16 UTC and renamed the fork so it would not turn up in a fork list search. A pull request opened at about 10:49 the next morning triggered the pull_request_target job; a force push at 11:11 landed the payload on the pull request head; at 11:29 a 1.1 GB cache entry was saved under the exact key the release workflow would later look up on main. At 11:31 the attacker force pushed the pull request back to a no-op and closed it. The poison stayed.
Seven hours and forty four minutes later a release workflow run restored that cache, and the malware inside it read the OIDC token out of the runner's memory and posted directly to the npm registry, bypassing the workflow's own publish step, which had been skipped because tests failed.
TanStack's summary of what this means for anyone relying on provenance as a control is worth quoting in full, because it is six words long: "Provenance shouldn't be confused with innocence." The publishes carried a valid OIDC trusted publisher binding. Two factor authentication was on. Lockfiles were in use. None of it touched the actual hole, which was the shape of a CI workflow.
The payload itself is what carries the story into CrowdSec's repositories. The advisory describes a roughly 2.3 MB obfuscated file named router_init.js, smuggled into the tarball root and deliberately left out of the package's declared files list, executed at install time through an optional dependency that resolves to an orphan commit in the router repository's fork network. GitHub serves commits across a whole fork network for git URL dependencies, so the attacker never needed write access to the real repository.
What it collects is the part to read twice. AWS instance metadata and Secrets Manager. GCP metadata. Kubernetes service account tokens. HashiCorp Vault tokens. The npm configuration file. GitHub tokens from environment variables, from the gh command line tool's configuration, and from .git-credentials. SSH private keys. Then it enumerates the packages the victim maintains and republishes them with the same injection.
The credential that mattered was not sitting in a repository
CrowdSec found the origin inside the stolen archive itself. The .github repository's git configuration still carried the clone URL it had been fetched with, and that URL contained an OAuth access token beginning gho_. GitHub's own engineering post on token formats confirms what the prefix means, and that each token carries a CRC32 checksum; CrowdSec checked the checksum and it was valid, which is why they were confident this was a real token rather than a decoy.
The token itself was gone. In CrowdSec's words, it "was created, lived, and died without leaving us any traces beyond its usage". It did not appear in the organisation audit log. It did not appear in the audit logs of any current member. The company also reviewed its list of authorised OAuth integrations, which it runs in restricted mode, and found nothing out of place.
GitHub support supplied the missing piece: the Git activity for the two hour window around the dump. It showed a fetch at 05:52:32.138 UTC on 22 May 2026, from an address in the 178.249.214 range, by the account of an employee who had just left the company and was still a member of the GitHub organisation.
CrowdSec is direct about why he was still there. "We kept them alive because we parted on good terms with our developer, and he wanted to finalize some work." Every other access he held had already been revoked. GitHub was the exception. The account was removed on 25 May, three days after the clone.
One further point is inference, and should be read as such. CrowdSec does not say which application minted the token. But the advisory lists the gh command line tool's configuration among the credential locations the payload reads, gho_ is the prefix GitHub uses for OAuth access tokens, and GitHub lists GitHub CLI among its privileged OAuth applications. A stored GitHub CLI credential on a developer laptop fits every observable in the report. It is not confirmed, and CrowdSec does not claim it.
Why the organisation audit log was blank
This is the part with the widest application beyond CrowdSec, because it is not a CrowdSec failing. It is documented platform behaviour, and most teams have never read the page that documents it.
CrowdSec had OAuth app access restrictions enabled. The name suggests exactly what you would want: the organisation decides which OAuth applications may reach its private data. GitHub's own documentation adds a carve out that is easy to miss. Some OAuth applications are privileged apps, owned by GitHub, and "even if an organization owner has restricted access by OAuth apps to the organization's data, users can still authorize these apps and use them to access data from the organization". The same page then says the quiet part: "These privileged apps will appear in the user security log, but will not appear in organization audit logs."
The list GitHub publishes runs to fourteen entries, and it is not a list of obscure integrations. It includes Gist, Git Credential Manager, GitHub for Android and iOS, GitHub CLI, GitHub Codespaces for JetBrains, GitHub Copilot CLI, GitHub Desktop, GitHub Education, GitHub Support, JetBrains IDE Integration, Visual Studio, Visual Studio Code and an internal importer service. Between them, those cover most of the ways a developer's machine actually holds a GitHub credential.
The second gap is retention. The events that would record a mass clone are git.clone and git.fetch. GitHub's audit log events reference notes that Git events "have special access requirements and retention policies that differ from other audit log events", that for GitHub Enterprise Cloud you access them "via the REST API only with 7-day retention", and that they are not available in the web interface at all. The general organisation audit log covers 180 days. Git events do not.
Three GitHub controls, the reassurance in the name, and what GitHub's own documentation says. Sources: GitHub docs on privileged OAuth apps, audit log events, and organisation personal access token policy.
| Control | What the name suggests | What GitHub's documentation actually says |
|---|---|---|
| OAuth app access restrictions | The organisation approves which OAuth apps reach private data | Privileged apps, fourteen of them and GitHub owned, are exempt, and their activity does not appear in organisation audit logs |
| Organisation audit log | A record of what happened in the organisation | Git clone and fetch events are Enterprise Cloud only, REST API only, with seven day retention, and are absent from the web interface |
| Personal access token policy | Tokens have a governed lifetime | Fine-grained tokens default to a 366 day maximum lifetime; classic tokens have no expiration requirement, and only fine-grained tokens are subject to owner approval |
Put the arithmetic together and the picture is bleak. The clone happened on 22 May. Even on an Enterprise Cloud plan, a seven day Git event window closes on or about 29 May. CrowdSec, which says in the same report that it is not on an enterprise plan, learned about the theft on 16 September. That is 110 days after any platform side Git record would have aged out. The only reason a timestamp exists at all is that GitHub's support team still had it and chose to help.
So the honest version of the detection story is this: a mass clone of every private repository in a GitHub organisation, by an account that has legitimate read access, is not an event most organisations can see, and is not an event most organisations can reconstruct afterwards.
What was in the archive, and what changed between the two statements
CrowdSec published twice. The first statement, on 17 September, said: "No client data, login/password, name, organization, or anything else was leaked". The full report, 24 hours later, revised that. The archive contained the email addresses of 83 users, kept by the data science team to monitor feature usage, and the first and last names, email addresses and investment context of 51 potential investors from 2020. That is 134 people, which is the figure the French breach tracker Fuites Infos used in its headline.
The first statement also got the mechanism the wrong way round. It said the TanStack component "was used in our organization in May and appears to have been backdoored to extract an API key with authorization to read the private codebase". The full report says the opposite about CrowdSec's own estate: some TanStack packages were present in its repositories, none were on the backdoored list, and the company enforces a minimum age on Node packages. The compromise happened on a person's machine, not in CrowdSec's build.
CrowdSec's statement of 17 September 2026 against its full report of 18 September 2026.
| Point | Stated 17 September | Stated 18 September |
|---|---|---|
| Personal data | No name, login or client data leaked | 83 user email addresses, plus names, emails and investment context for 51 potential investors from 2020 |
| Route in | A TanStack component used in the organisation appears to have been backdoored | No backdoored version was in CrowdSec's repositories; a departed employee's laptop was compromised |
| Credentials | Hunted for tokens enabling lateral movement, none found so far | One live AWS token, scoped to a single SNS topic, was found and was probed on 17 August 2026 |
| Certainty about the token | The leak was only exploitable during a short timeframe in May 2026 | GitHub traced the full lifecycle of a gho_ OAuth token used by a former employee's account |
The credential detail is instructive on its own. The one usable secret in roughly 3.4 GB of non-public code was an AWS role named assertible-zapier-sns-sender, restricted to publishing on a single SNS topic. Someone tried it on 17 August 2026 from 23.234.84.102, calling GetCallerIdentity and ListTopics. It went no further.
That probe is 87 days after the clone and 30 days before the archive was posted publicly. Whoever held the archive was working through it methodically, a month before anyone outside knew it existed.
Method, accusation and commercial interest
CrowdSec sells security products, and the report is written by its chief executive. It opens with several paragraphs about product market fit, conversations with governments in at least seven countries, and the support of the community, and it closes on the observation that the company walks out "stronger and sharper". None of that is evidence. A vendor publishing its own incident report is doing communications as well as disclosure, and it would be naive to read it otherwise.
Separate the method from the framing, though, and the forensic section is unusually checkable. It names the token prefix and the exact command to search your own audit log export for a hashed token. It names the AWS role, the probing address, the timestamps to the millisecond, and a repository that had been destroyed on 27 May and appears in the archive anyway, which is how the archive's date was pinned. Very few companies publish that much about their own bad day.
The correction is the part that earns the most credit. The first statement was wrong about personal data, and the second said so in public, in the same place, within a day, and named the 51 investors as the people CrowdSec was most sorry for. That is not what a communications team optimising for a quiet week would produce. It is also, practically, what made this story usable: the arithmetic in this briefing only works because CrowdSec published timestamps precise enough to do it.
The unverifiable parts should be labelled as such. The attribution of the package compromise to a group CrowdSec calls TeamPCP, tracked as UNC6780, the naming of the individual who posted the archive, and the claim that the same actor hit Mistral AI repositories on 22 May all come from CrowdSec's timeline. We could not reach a primary source for any of the three. They are plausible and consistent with the campaign, and they are still one company's account.
What to do this week
In the order worth doing, and grounded in GitHub's own documentation rather than in general advice. The first three are the ones that would actually have changed this outcome.
Take this with you
GitHub organisation and token governance
- Pull the member list of every GitHub organisation you own and reconcile it against your HR leavers report for the last 12 months. Removal belongs on the leaving date, not on the date the last handover task finishes.
- Where a leaver genuinely needs a few more days on one repository, move them to that single repository as an outside collaborator rather than leaving full organisation membership in place.
- Read GitHub's documented effects of removing a member before you rely on removal alone. Removed members lose access to private forks but may still hold local copies, and a private repository forked into another organisation can keep access through that fork network.
- Set an organisation personal access token policy. Restrict access by classic personal access tokens, because GitHub states they have no expiration requirement and are not subject to owner approval.
- Enable the requirement that an owner approves every fine-grained personal access token that can reach the organisation, and set a maximum lifetime shorter than the 366 day default.
- Check whether OAuth app access restrictions are enabled, then read GitHub's privileged apps page and accept that those fourteen GitHub owned applications sit outside the restriction and outside your audit log.
- Ask each engineer to review their own GitHub security log, which is the only place a privileged app authorisation is recorded, and to revoke authorisations they no longer use.
Take this with you
Detection and developer machines
- If you are on GitHub Enterprise Cloud, stream git.clone and git.fetch to your SIEM today. With seven day retention and REST API only access, a weekly manual export is not a control.
- Write one alert on volume, not on identity: a single actor fetching more than a threshold of distinct repositories inside an hour, at any hour. A nine minute burst across 170 repositories is a shape, not a subtlety.
- If you are not on an enterprise plan, write down that you have no repository clone telemetry, put it on the risk register, and price the upgrade against that line rather than against a feature list.
- Export your organisation audit log and search it for hashed tokens rather than raw ones. CrowdSec published the exact command, and the hashed form is what the log contains.
- Treat any developer or CI host that installed a flagged package as fully compromised, not compromised for the one credential you happened to find. The TanStack advisory lists nine distinct credential locations the payload reads.
- Inventory where developer credentials actually live on a laptop: the gh CLI configuration, .git-credentials, the npm configuration file, the SSH directory, environment variables and shell history. That list is the real blast radius of any install time payload.
- Enforce a minimum package age in your package manager. CrowdSec had this control and credits it for keeping the backdoored versions out of its own repositories.
- Put endpoint detection on every machine that touches your codebase or infrastructure. CrowdSec did not have it at the time and has since added it.
- Audit your own repositories for pull_request_target workflows that check out a fork ref, and for any workflow that both restores a shared cache and holds id-token write permission. A static analyser for Actions workflows as a required pull request check is cheaper than the audit.
- Scan private repositories for live credentials on the assumption that a copy of them is already readable by someone, and rotate what you find rather than noting it.
The question that exposes the gap
CrowdSec did almost everything on the conventional list. Strict privilege separation, to the point that the chief executive has no access to the AWS console. Tightly scoped tokens. Two factor authentication everywhere, with passkeys and hardware keys. Password managers. Routine internal and external audits. Pentesting. A minimum age policy on Node packages. Automated scanning of every commit. Written onboarding and offboarding procedures.
One account stayed in a GitHub organisation for a few extra days as a courtesy, on a machine the company no longer controlled, and that was the whole attack. There was no exploit against CrowdSec, no privilege escalation, no lateral movement. Read access, used as designed, at speed.
So the question for your own organisation is not whether you would have stopped it. It is this: if a former colleague's laptop cloned every private repository you own at six o'clock tomorrow morning, which system would tell you, and how many days would you have to ask it before the answer stopped existing?
Sources
- PrimaryCrowdSec's full incident report, 18 September 2026: the timeline, the gho_ OAuth token found in the archive's git config, the GitHub support trace to a departed employee, the AWS SNS token probe, the personal data in the archive and the controls in place before the incidentCrowdSecaccessed 2026-09-19
- PrimaryCrowdSec's first statement, 17 September 2026, used for the initial no personal data claim and the initial attribution of the leak vector, both of which the 18 September report revisesCrowdSecaccessed 2026-09-19
- PrimaryThe GitHub Security Advisory record for CVE-2026-45321, used for the 84 versions across 42 packages, the publish window, the credential harvesting list including gh CLI config, the exfiltration network and the full affected version tableOpen Source Vulnerabilities (OSV)accessed 2026-09-19
- PrimaryTanStack's postmortem, 11 May 2026 and revised 15 May 2026: the minute by minute attack chain, the three chained weaknesses, the detection timing and the open questionsTanStackaccessed 2026-09-19
- PrimaryTanStack's hardening companion piece, 12 May 2026, used for the statement that no maintainer token was stolen and for the list of changes already madeTanStackaccessed 2026-09-19
- PrimaryNVD record for CVE-2026-45321, used for the publication date, the analysed status and the CVSS 9.6 base score assigned by GitHubNIST National Vulnerability Databaseaccessed 2026-09-19
- PrimaryKnown Exploited Vulnerabilities catalogue, version 2026.09.18, used for the CVE-2026-45321 entry, the 27 May 2026 addition date and the 10 June 2026 remediation due dateCISAaccessed 2026-09-19
- PrimaryGitHub documentation on privileged OAuth apps, used for the statement that these apps bypass OAuth app access restrictions and do not appear in organisation audit logsGitHubaccessed 2026-09-19
- PrimaryGitHub audit log events reference, used for the git.clone and git.fetch events, their REST API only availability and the seven day retention for Git eventsGitHubaccessed 2026-09-19
- PrimaryGitHub documentation on organisation personal access token policy, used for the 366 day fine-grained default maximum lifetime, the absence of any expiration requirement for classic tokens and the approval policy that covers fine-grained tokens onlyGitHubaccessed 2026-09-19
- PrimaryGitHub documentation on removing an organisation member, used for the local copies caveat, the fork network caveat and the three month membership data retentionGitHubaccessed 2026-09-19
- PrimaryGitHub documentation on OAuth app access restrictions, used for what the control does and for the explicit carve out for privileged appsGitHubaccessed 2026-09-19
- PrimaryGitHub engineering post on token formats, used to confirm that the gho_ prefix denotes an OAuth access token and that tokens carry a CRC32 checksumGitHubaccessed 2026-09-19
- PrimaryICO guidance on personal data breaches, used for the UK GDPR 72 hour notification clock running from awareness and for phased reporting under Article 33(4)Information Commissioner's Officeaccessed 2026-09-19
- Reported byThe French breach tracker that notified CrowdSec, used for the archive size of about 3.4 GB of non public code and the count of 134 people exposedFuites Infosaccessed 2026-09-19
- Reported byNews coverage, 19 September 2026, which brought the CrowdSec report to a wider audience and framed the 170 repository figureThe Hacker Newsaccessed 2026-09-19


