Skip to content
Christian Schneider

Christian Schneider

christian-schneider.net June 8, 2026

Read on if your build pipelines auto-adopt new dependency versions — a zero-cost delay policy eliminates most supply chain attack windows.

Most supply chain attacks follow the same arc. Malicious code lands on a package registry. Within hours it has been downloaded thousands of times. By the time anyone flags it, the damage is done.

This is the golden hour of supply chain attacks: the window in which attackers race to compromise systems before their package gets detected and pulled. They exploit the immediate-adoption habits of most CI/CD pipelines. A popular package publishes a new version, pipelines worldwide pull it within minutes, and the attacker has all the time they need.

Then March 2026 happened. Three major compromises in under two weeks:

April 2026 added more to the pile:

May 2026 then linked upstream pipeline security and downstream cooldown policies in the clearest way yet:

The TanStack compromise (May 11, 2026) was a Mini Shai-Hulud variant that pushed 84 malicious versions across 42 @tanstack/* packages in roughly six minutes. The entry point was a pull_request_target workflow in TanStack/router that ran fork-controlled code with elevated trust, combined with actions/cache poisoning across the fork-to-base trust boundary and runtime extraction of an OIDC token from the runner process. The attacker then used the project’s trusted-publisher binding to publish straight to npm. @tanstack/react-router alone pulls roughly 12 million weekly downloads, so the blast radius was large. Maintainers and npm yanked the malicious versions within hours after the first community reports. Wiz attributes the campaign to TeamPCP, the same crew behind Trivy, Checkmarx, and several other 2025–2026 incidents.

Two things are worth pulling out. First, this is the exact upstream-pipeline failure mode I covered in my Ship fast, but guard faster post: pull_request_target with a fork checkout is the classic “Pwn Request” anti-pattern, and permissions: contents: read does not stop actions/cache from writing across the fork-to-base trust boundary. Second, even with a clean compromise at the source, downstream consumers who held off on accepting fresh versions for a few days were never exposed. The malicious releases were already gone before their pipelines would have considered them. Cooldowns do not fix the upstream maintainer’s pipeline. They keep your project from being collateral damage when somebody else’s pipeline fails.

The downstream consequences of the TanStack push started surfacing within days. The most public confirmation came from Grafana Labs:

Grafana Labs disclosed on May 16 that attackers tied to the TanStack campaign had reached parts of its GitHub environment. The Grafana team detected suspicious activity on May 11 — the same day the malicious @tanstack/* versions hit npm — and rotated what it described as “a significant number” of GitHub workflow tokens in response. They missed one. Attackers used that single leftover token to keep accessing repositories and then sent Grafana a ransom demand on May 16. The Unit 42 write-up on the broader campaign explains why the rotation set was so hard to get right: the May 11 wave chained pull_request_target with actions/cache poisoning, then extracted an OpenID Connect token from runner memory before publishing malicious packages. So the credentials in scope for Grafana’s response weren’t a long-lived PAT or two. They were a mix of static workflow secrets, short-lived OIDC bindings, and runner-internal artifacts spread across many workflows.

A 7-day cooldown on newly published @tanstack/* versions would likely have kept most downstream consumers outside the initial malicious publication window. For organizations whose pipelines had already resolved the compromised versions before the takedown, the -best control is a rotation playbook that actually verifies the rotation finished. Grafana caught the cascade on day one and still left one credential behind. That is the part of this incident worth coming back to in the identity-hygiene section below.

Eight days later the same campaign moved to PyPI, this time with an official Microsoft package as the carrier:

The durabletask compromise (May 19, 2026) hit Microsoft’s official Python SDK for Azure Durable Functions, a package with roughly 400K monthly downloads. The attacker pushed three malicious versions directly to PyPI inside a 35-minute window, with no matching tags or workflow runs in the GitHub repository. The release pipeline was bypassed entirely, which points to a stolen PYPI_API_TOKEN or a compromised maintainer account. The project did not use PyPI’s Trusted Publishing (OIDC). The injected dropper sits in __init__.py and only fires on Linux, so the real target is CI runners and cloud workloads rather than developer laptops. It pulls down a 28 KB modular framework that harvests credentials from AWS, Azure, GCP, Kubernetes, Vault, and the local password managers (Bitwarden, 1Password, pass, gopass), then propagates to additional hosts via aws ssm send-command and kubectl exec . Microsoft yanked the affected versions within two hours of the first upload.

Two things to pull out of this one. First, an official Microsoft package, with hundreds of thousands of monthly downloads and a real maintenance team behind it, still shipped malicious releases for roughly two hours before being pulled. Brand and provenance buy you a faster takedown, not a safer package. The only reliable defense against a two-hour compromise window is not being inside it: a 7-day cooldown on PyPI updates would have kept consumers from ever resolving the compromised versions. Second, this is the textbook argument for PyPI’s Trusted Publishing on the publisher side. A long-lived PYPI_API_TOKEN is one secret an attacker can lift once and use forever, while an OIDC binding only authenticates uploads that actually originate from the configured CI/CD job, on the configured tag and workflow. If you maintain a PyPI package, this incident is the cue to migrate.

Same shape across all of these. Same two defenses, too: dependency cooldowns for short-window publication attacks, immutable pinning for tag-rewrite attacks.

A dependency cooldown is exactly what it sounds like: a waiting period before your tooling accepts new package versions. Instead of immediately adopting version 1.2.4 when it’s published, you wait 5 to 10 days before considering it for your project.

The approach works because the attacker is racing the clock. Registry security teams, automated scanners, and the broader security community are all looking for suspicious packages all the time. Most get detected and removed within 24–72 hours. The timestamps from every incident above bear this out. A 7-day cooldown keeps your pipeline out of that window.

The math is simple: if malicious packages disappear within 24–72 hours, a 7-day delay puts your pipeline outside the danger window every time. The teams running cooldown policies during the Nx incident were never exposed. The malicious versions had been pulled days before their pipelines would have considered them.

Be precise what cooldowns solve. They address version freshness risk : the risk of blindly adopting new, unvetted releases. They do not mitigate known vulnerabilities already in your tree. Once a fix lands for a published CVE, the calculus flips and delay becomes the dangerous side.

Every one of these incidents triggered the same downstream cleanup for thousands of organizations: rotate credentials, audit logs, hunt for persistence, write the postmortem. A cooldown policy costs nothing and skips the entire exercise.

Several dependency management tools now support cooldowns natively:

Dependabot introduced the cooldown option in mid-2025, allowing you to specify minimum age requirements before version updates are proposed. You can configure different delays based on semantic version changes, with longer waits for major versions and shorter ones for patches. Dependabot’s cooldown applies only to routine version updates, not security updates, so CVE patches should still flow through promptly. See the Dependabot cooldown documentation for configuration details.

Test the actual behavior in your own repos before relying on it. Cooldown logic is applied at update runtime, and overly broad configuration or exclusions can silently suppress updates you actually wanted.

Renovate has the same control under minimumReleaseAge (previously called stabilityDays ). Renovate creates branches for pending updates but marks them with a “pending” status check until the cooldown expires. If you have automerge enabled, updates won’t merge until they’ve aged enough. In Renovate 42: packages without a release timestamp are now treated as if they haven’t passed the cooldown period, which is safer than the behavior (where missing timestamps fell through). The Renovate minimum release age documentation covers the configuration options.

In Renovate setups with broad package rules, security updates can still sit “pending” unless explicitly excluded from cooldown logic. Add explicit security-specific rules that bypass the cooldown.

pnpm added the minimum-release-age setting in version 10.16. It filters packages by publish date and automatically remaps dist-tags to versions that meet the age requirement. Semver ranges keep working; the delay still gets enforced.

For ecosystems without native cooldown support, lock files provide a manual alternative. Tools like Poetry, uv, or Go modules with go.sum pin exact versions, including transitive dependencies, so newly published releases are never pulled in implicitly. Even on a weekly or bi-weekly cadence, the refresh becomes a deliberate step: update the lock file, review the diff, accept the newer versions. The cooldown emerges from the process. Treat dependency updates as a reviewable activity, not background plumbing.

One recurring failure mode I see in audits is teams enabling cooldowns, assuming they are “safe,” and then relaxing their active monitoring of security advisories. Cooldowns reduce exposure to unknown malicious releases. They do nothing for known vulnerabilities already present in your dependency tree.

Without active vulnerability alerting and triage, cooldowns can actually increase dwell time for exploitable CVEs. Cooldowns are a preventive control, not a detective one.

Cooldowns must apply to your entire dependency graph , not just direct dependencies. A malicious package introduced as a transitive can still reach production even when your direct imports are carefully curated.

Dependabot and Renovate handle this if you use them with lockfiles and conservative update policies. They operate on the resolved dependency graph, so updates (including transitives) show up as lockfile changes rather than flowing in silently. As long as lockfiles are committed and updates are gated, nothing transitive changes without you accepting it.

The anti-pattern: floating transitive dependencies in production with cooldowns only on direct dependencies. That just moves the golden-hour problem one level down the graph, which is exactly where attackers increasingly aim.

If you rely on manual version pinning or ecosystems without strong lockfile enforcement, this safety net disappears. Rebuild and review the full dependency graph on a schedule (for example via mvn dependency:tree , pip-compile , or equivalent tooling) to catch unexpected transitive additions or version shifts.

Cooldowns work best when paired with an explicit security SLA, for example: critical dependency CVEs must be triaged within 24 hours and patched within 72 .

Cooldowns should apply to routine updates, not emergency security patches. Dependabot explicitly excludes security updates from cooldown rules. Renovate allows you to force immediate updates for specific packages through its Dependency Dashboard or security-specific rules.

Bypasses need a paper trail. The security team approves, the reason gets logged. When this comes up in an audit later, the question won’t be “did you patch fast?” but “why did you skip the cooldown?” Have the answer ready.

Fast-tracked packages get extra scrutiny. Review the diff manually or with tooling: obfuscation, dynamic code execution, unexpected network calls, new persistence mechanisms. Once the normal cooldown period expires, recheck the package against whatever fresh advisories have surfaced in the meantime.

Worth being precise the limits.

Dependency cooldowns are effective against:

They are not effective against:

Cooldowns buy you time , not certainty . Use that time to let scanners run, advisories surface, and the community react. Then decide from a position of information, not urgency.

Cooldowns are one layer. The rest of the usual stack still applies: SBOM generation, vulnerability scanning, signature verification, dependency audits. Yes, Trivy itself was compromised in March 2026. The irony isn’t lost on me. The scanner is still solid — the distribution channel was the problem, and pinning to commit SHAs would have prevented it:

As the Trivy incident showed, cooldowns don’t help when the attacker rewrites existing version references rather than publishing new ones. Git tags are mutable. Commit SHAs are not. The fix: pin GitHub Actions (and any Git-referenced dependency) to full commit SHAs instead of version tags.

Instead of: uses: some-org/some-action@v1 use full commit SHA: uses: some-org/some-action@28f2510ee396bbf400402947e7a9c4e3f7e3b144

It looks ugly, and that’s intentional. The full SHA forces an explicit, reviewable decision every time something is updated. Dependency management tools can handle this: pin to full commit SHAs, still enforce cooldowns, propose updates as new versions appear. The workflow stays sane.

GitHub’s Immutable Releases feature helps, but it is not a substitute for pinning. In the Trivy incident, some artifacts were protected by immutable releases, while mutable tags in GitHub Actions were still force-pushed to malicious commits. Pin to the full SHA.

For npm, the lockfile integrity hashes ( sha512-... in package-lock.json ) provide a similar control: they bind installs to specific resolved artifacts. That’s why committing your lockfile and using npm ci matters. npm ci requires an existing lockfile, refuses to reconcile mismatches by rewriting it, and performs a frozen install. In practice that blocks most silent drifts to newly published malicious versions, provided the lockfile was committed before the compromise.

Pin to content-addressed references wherever possible. Version tags, branch names, and semver ranges are all mutable indirections. Commit SHAs and integrity hashes are not.

IDE extensions are dependencies. They just don’t show up in your package.json , pom.xml , requirements.txt , or go.mod , so nobody treats them that way. But a VS Code extension with workspace trust reads your files, sees your environment variables, and can reach locally stored credentials. Many have terminal and network permissions. And unlike your CI/CD dependencies, they update silently in the background — no pull request, no lockfile diff, no review step.

Eighteen minutes. That was the entire exposure window. “Verified publisher” meant nothing here — the legitimate publisher’s credentials were the weapon.

If you’ve been following the cooldown argument through this post, the pattern should feel familiar by now. If VS Code hadn’t auto-updated the moment the compromised version landed on the marketplace, nobody would have been exposed. The malicious release was pulled faster than most standups run. The only developers hit were the ones whose IDE silently adopted it in real time. Same golden-hour economics, different distribution channel.

Short-lived compromises aren’t the only threat model, though. Researchers linked the GlassWorm campaign to dozens of malicious or dormant Open VSX extensions starting in late 2025, including sleeper packages that looked benign for months and then slipped malicious payloads in once they had earned install counts and trust. The Prettier-VSCode-Plus attack (November 2025) delivered multi-stage malware through a convincing Prettier clone. A cooldown catches both patterns: the delay gives scanners and the community time to flag problems before the extension ever reaches your machine.

Extension marketplaces also have weaker vetting than package registries. npm, PyPI, and crates.io run automated malware scanning and support Trusted Publishing flows. The VS Code Marketplace’s “verified publisher” badge confirms the publisher’s domain ownership — it says nothing what the code in the .vsix actually does. Open VSX checks even less. The Checkmarx compromise I covered above hit their VS Code extensions on both marketplaces, and the repeat incident a month later showed that once a distribution channel is identified as a soft target, attackers come back. When a package registry and an extension marketplace both face supply chain pressure, the marketplace cracks first.

The Grafana case above illustrates a failure mode that no cooldown policy can save you from: you are the downstream consumer of somebody else’s compromised pipeline, you detect the cascade on day one, you rotate tokens — and you still miss one. From there, a single leftover credential is enough to keep the attacker inside your perimeter for days.

This pattern shows up again and again in public CI/CD incident write-ups. Teams treat “rotate tokens” as a cleanup task rather than as an engineering problem with verification requirements.

Cooldowns, pinning, IDE extension hygiene, and identity hygiene are stacked controls for different failure modes. Cooldowns keep you out of compromised upstream windows. Pinning blunts tag-rewrite attacks. A verified rotation playbook is what saves you when somebody else’s pipeline already failed and the cascade reached you anyway.

For more practical tips on locking down your supply chain and secure build pipelines, see my post how to ship fast, but guard faster: securing DevOps itself .

Rule of thumb: Delay unknown updates by default, fast-track known security fixes deliberately.

If you take nothing else from this post, set a 7-day cooldown on your automated CI/CD dependency updates and IDE extension updates this week. The config is minimal. The protection is immediate.

For teams worried being “slowed down”: you’re already waiting days or weeks between dependency updates in practice. Cooldowns just formalize that delay and make sure it applies consistently, including on the one rushed Friday afternoon deploy nobody wants to think .

Attackers are counting on you to adopt their malicious packages immediately. Make them wait.