Buying Time: Package Managers Are Adding Delay as a Security Control
Composer 2.11 shipped a cooldown that refuses to install a version until it has aged. vlt now holds npm publishes until their dependencies actually exist, because 22% of new npm versions land broken. And Ruby Central's reviewers threw out 90% of what an AI scanner flagged.
Every supply-chain defence I've watched get built in the last ten years has been a version of the same bet: that we can look at a package and tell whether it's out to get us.
Scan it. Diff it. Check the signature. Sandbox it. Feed it to a model. Find the bad thing, block the bad thing.
Three things landed last week that quietly bet the other way. Not "we'll catch it." Just — we'll wait, and let somebody else catch it.
That's a much more pessimistic position than it sounds like. I also think it's right, and I want to show you why, because the reasoning behind one of them is sharper than anything I expected to read in a release note.
What actually shipped
Composer 2.11 got a cooldown policy — a configurable window where a freshly published version just isn't a candidate for install. vlt started holding npm publishes until their transitive dependencies resolve. And Ruby Central published numbers from pointing AI models at the most-downloaded gems — numbers that make the case for the other two better than either of them does.
One at a time they're three release notes. Together they're an admission, and I don't mean that as a dig.
Composer's cooldown, and the bit that's actually clever
The config lives under config.policy.cooldown:
{
"config": {
"policy": {
"cooldown": {
"period": "1 day"
}
}
}
}
period takes a string with units of second, minute, hour, day or week — "6 hours" is fine — or a plain integer of seconds. You can set it globally or with COMPOSER_POLICY_COOLDOWN_PERIOD. There's an ignore key for exempting packages, and it does reasons, version constraints and wildcards.
It's not on by default. Right call for a point release, but it also means nothing changes for you until you go switch it on.
Why a cooldown works at all
Before the clever part, the premise, because this is where I'd normally be sceptical. Why would waiting help?
Packagist laid this out back in May, months before the feature shipped, and the answer is one sentence: "malicious versions are typically pulled or detected within hours of publication."
That's the whole bet. Not that you'll spot the malware — that somebody will, fast, and if you simply weren't installing during those hours, you never had the problem. They described the planned policy as refusing "to install a version that was published less than N hours or days ago," which is exactly what shipped.
And the threat model underneath it is more specific than I assumed. They name two PHP incidents — laravel-lang on May 22 and intercom/intercom-php on April 30 — and note that many of these attacks involved attackers changing existing git tags after publication.
Sit with that for a second, because it reframes the whole thing. The attack isn't always "publish a malicious new version." Sometimes it's "quietly change what an already-trusted tag points at." The version number you pinned doesn't move. The code behind it does.
Which explains something I'd written off as an annoyance. The cooldown needs to know when a version was published, and Packagist says the feature was blocked on prerequisite work to make release metadata reliably immutable. Of course it was. A cooldown keyed on publish time is worthless if an attacker can rewrite the publish time. The boring metadata plumbing had to land before the security feature could mean anything, and that's why repositories that don't report a published-time make the command fail rather than quietly passing.
If you run a private Satis or Artifactory, that's your landmine: test it in CI before it blocks a deploy at two in the morning. You can set require-published-time to false for a repository you trust, which falls back to the time field.
Worth knowing too: Packagist has been importing malware detection results from Aikido since March 2026. Flagged versions show up prominently in the UI and in the metadata served to Composer, and Aikido flagged the malicious versions in each of those recent incidents. So the cooldown isn't the only layer — it's the layer that covers the gap before the other layer notices.
Now the clever part
Your first thought reading "cooldown" was probably mine: fine, but it had better not delay security patches. A one-day cooldown sitting on the fix for a critical RCE is trading one risk for a worse one.
Composer's answer is that security fixes do go through the cooldown. And the reasoning stopped me:
Composer cannot safely let security fixes skip the cooldown policy automatically
Think about what the exemption would have to be. Composer reads advisory data, sees "this version fixes a vulnerability," waives the delay. Now: an attacker who's compromised a repository publishes a malicious version and the metadata declaring it a security fix. The exemption is the lockpick.
I've been chewing on that one all week, because it generalises way past Composer. Every automatic "except in emergencies" rule is a bypass if the attacker controls the thing that declares the emergency. I can think of three systems I've touched that have exactly that hole in them.
So Composer makes it your call instead — --no-cooldown on update, require, remove and create-project, or a permanent ignore for packages you trust. And the flag is scoped tight: "Blocking of versions with security advisories, malware and your other policies stays active." You're skipping the wait, not switching off the guardrails.
| Composer cooldown | vlt publish hold | |
|---|---|---|
| What's delayed | Installing a new version | Releasing a new publish |
| Who decides | The consumer | The registry client |
| Default | Off | On, one-hour cap |
| Why | Malicious versions get pulled fast | Broken dependency graphs |
vlt got here from somewhere completely different
The npm side arrived at the same instrument for an unrelated reason, and the number that drove it is worth the whole article.
In one 24-hour window, vlt counted 175,891 new versions published to npm. Of those, 38,995 — 22% — "landed on the registry with a dependency that could not be resolved."
More than a fifth of everything published to npm in a day was, at the moment it was published, uninstallable. Not malicious. Just broken, because the thing it depended on wasn't there yet.
I have lost mornings to that failure mode without ever once knowing that's what it was. You add a package, the install detonates on something three levels down that doesn't exist, and you assume it's you. You go read your own lockfile. You try a different version constraint. You start googling the transitive dependency's name with "ETARGET" after it. And the actual answer was "come back in ten minutes." Nobody tells you that, because nobody knew — the registry had no way to say "this is temporarily incomplete" as distinct from "this is wrong."
vlt's fix is to hold a publish until its transitive dependencies resolve. 96% of coordinated releases finish "within a few minutes," and the remaining 4% "can wait up to an hour before we time out and release them" anyway. It handles circular dependencies and ignores mutable non-registry stuff like git URLs.
That's not an anti-malware measure at all. It's a consistency measure — the registry equivalent of not committing a transaction halfway through. But look at the shape of it. Two ecosystems, two completely unrelated motivations, same instrument: put latency between publication and consumption. Nobody coordinated that.
Why delay is winning: the Ruby Central numbers
Here's the case for pessimism, and it's the most useful data of the week.
Ruby Central has been running AI models across a curated list of 619 heavily-downloaded gems — 158 billion downloads between them, 26 of which have individually cleared a billion. They're about 317 of the way through.
| Stage | Count |
|---|---|
| Gems on the list | 619 |
| Scanned so far | ~317 |
| Findings reviewers evaluated | 134 |
| Rejected | 120 (90%) |
| Escalated to maintainers or advisories | ~15 |
| Advisories opened | 6 |
Their words: "Of the 134 findings our reviewers evaluated, 120 (90%) were rejected because they weren't reachable by an attacker" — plus duplicates and intended behaviour.
Ninety percent. And notice who rejected them. Human reviewers, reading code, deciding whether a flagged path was actually reachable. The scan isn't the expensive part of this pipeline. The triage is.
That number tells you what these tools are, and it's not what they get sold as. A scanner with a 90% false-positive rate isn't useless — Ruby Central got six real advisories out of it. But it is absolutely not a gate you drop in front of a registry and walk away from. Put that in CI as a blocking check and what you've actually built is a machine for training your team to click through security warnings, which is worse than having no check at all.
Which is exactly why a cooldown looks so good next to it. A cooldown has no false-positive rate. It doesn't need to understand your code, or anybody's code. It just buys 24 hours for somebody else's human reviewer — or for a maintainer to notice their account got popped.
The one that was real
The findings that survived triage justify the whole exercise, and the public one is a beauty.
rubyntlm. Over 94 million downloads. Its NTLMv2 password check ignored the password it was handed and validated logins against an empty password instead. Anybody who knew a valid username could walk straight in. Maintainer rated it critical, 9.3 out of 10. Fixed in rubyntlm 0.6.6.
An authentication bypass in a gem with eight figures of downloads, found by pointing a model at code nobody had re-read in years.
Both things are true at the same time, and you have to hold both: the tool has a 90% noise rate, and the tool found something years of human attention walked right past. Anybody selling you only one of those halves is selling you something.
The other half of the week: proving where things came from
Delay wasn't the only pattern going around. Ruby Central shipped two things squarely in the verification camp and there are ideas worth stealing in both.
Aaron Patterson audited all 180,000-plus published gems to build a metadata schema from what's actually out there, and gem pushes now get validated against it. The design note is the valuable bit:
The validator extracts only the values it expects from a gem's metadata instead of letting Ruby build objects from it.
If you've ever dealt with a deserialization CVE, that sentence is the entire lesson in one line. Don't hand attacker-controlled data to a constructor. Pull out the fields you want and ignore everything else. Same class of fix as the PHP flaw that fires during request initialization — the bug lives in the machinery that builds your objects, upstream of anywhere your validation runs.
The second one's a good incident writeup, and I wish more projects wrote them this plainly. Luke Marshall of Truffle Security reported that responses from the legacy API key endpoint could get cached by the CDN and served to the wrong person for up to an hour. Fixed in three days. Endpoint retired, every legacy API key revoked, everyone who'd ever held one notified. Their log review: "Our access logs show no sign of a legacy key being used maliciously."
Revoking an entire class of credential is the unglamorous correct answer and it buys you a week of support tickets. Credit where it's due — that's a decision somebody had to defend internally. Practical note: if gem push, gem yank or gem owner suddenly throws a 401 from an older setup, this is why. Make a new key.
Homebrew 7.0.11 went the same direction, verifying formula patch downloads from the API and requiring verifiable sources for official formulae.
What a cooldown doesn't do
It isn't malware detection. A malicious version nobody reports installs happily on day two. The whole bet is that the gap between "attacker publishes" and "somebody notices" is short — which has held up well for the loud npm compromises and holds up badly for a quiet backdoor in a package nobody reads.
It does nothing about an old version. If the compromise is in a release from eight months ago, every cooldown on earth has already elapsed.
It doesn't help with typosquatting, because the squatted package is a legitimately published package that happens to be called requsts. Age doesn't make it less hostile.
And it isn't a complete answer to the git-tag mutation problem either, which is the one I'd actually worry about. If an attacker rewrites what an existing tag points at, a cooldown on new versions only helps you if the metadata correctly reflects that something changed — which is the immutability work, not the cooldown.
And it costs you something real. A cooldown means you are deliberately not running the latest patch. Most weeks that's free. The week there's an actively exploited RCE in a dependency, your cooldown is working against you, and you'll be reaching for --no-cooldown under precisely the time pressure where people fat-finger things. Decide now who's allowed to use that flag and write it down somewhere that isn't one person's memory.
What I'd actually do
Ship PHP? Turn it on. Start short — six hours or a day — and set ignore entries for your own internal packages, which gain nothing from waiting. Test the published-time behaviour against your private repos first.
Ship anything else? The Composer config isn't yours to use, but the idea travels fine: pin, and separate "update the lockfile" from "deploy." Most teams already own the machinery for that and just run both in the same step because nobody ever told them not to. The npm trust-decision piece goes further on where that boundary belongs in CI.
And whatever you do, don't read the Ruby Central numbers as "AI scanning doesn't work." Read them as what they are: six real advisories and a 90% triage burden. If you're planning to point a model at your own dependencies, budget for the human who reads 134 findings to find 14. That person is the product. The model is the cheap part, and everybody gets that backwards.
The thing I keep coming back to is that three ecosystems arrived at latency independently, for three different reasons, in the same week, with nobody coordinating it. In my experience that usually means the idea is correct.
It also means somebody's about to write a very confident post calling it an anti-pattern. Enjoy the next fortnight.
$ tail -f /forums
Discussion
Comments live on the community forums — sign in there to reply.