Cybersecurity

The Same Researcher Broke apt the Same Way Twice, Seven Years Apart

In 2019 Max Justicz showed apt would install a package an attacker swapped in over a redirect. In November 2026 he did it again, CVE-2026-109518, through a different door that ends in the same place: code as root from a man in the middle. The fix shows how apt reuses files on disk.

brown cardboard boxes on a white metal rack
Photo by CHUTTERSNAP / Unsplash

The promise of signed packages is that the network cannot hurt you. Your machine downloads a .deb over whatever transport, checks it against a signature chain rooted in a key you already trust, and only then hands it to dpkg to install as root. A man in the middle can corrupt the bytes in flight all day; the signature check catches it, the install never happens, and the worst outcome is a failed download. That's the entire reason apt over plain HTTP was ever considered acceptable: the transport doesn't need to be trusted because the content is verified.

CVE-2026-109518 breaks that promise, and it does it in the gap the promise never mentions — the moment between "verified" and "installed." A network attacker can let a package pass verification and then replace it with a different one before dpkg runs it. The result is arbitrary code execution as root on a machine doing nothing more unusual than apt upgrade. It was reported by Max Justicz, scored 7.7 High by Canonical, and patched across Debian and Ubuntu on 4 November 2026.

If that name and that shape sound familiar, they should. Max Justicz did this to apt once before.

Twice Through the Same Door

In January 2019, Justicz reported CVE-2019-3462. apt fetched packages over HTTP and followed redirects, and the redirect handling let a man in the middle inject a response that both pointed apt at attacker content and supplied the hashes apt would check it against. Verification passed because the attacker controlled both sides of the comparison. Root followed. The mitigation at the time was to turn redirects off entirely:

# The 2019 mitigation (CVE-2019-3462)
sudo apt update  -o Acquire::http::AllowRedirect=false
sudo apt upgrade -o Acquire::http::AllowRedirect=false

Justicz argued then that HTTPS repositories should be the default, and that users should have to opt down to less security rather than opt up to it. The argument didn't win. Plain-HTTP mirrors with signature verification stayed the norm, on the theory that the signature was enough.

Seven years later, the same researcher, the same program, the same man-in-the-middle-to-root outcome — through a completely different bug. The two are worth putting side by side, because the difference is the whole story:

CVE-2019-3462 (2019) CVE-2026-109518 (2026)
Researcher Max Justicz Max Justicz
Entry point HTTP redirect in the http method HTTP redirect merging two downloads
Bug class Verification bypass Time-of-check to time-of-use (CWE-367)
When the swap happens During verification (attacker supplies hashes) After verification (attacker overwrites the checked file)
Outcome Code execution as root Code execution as root
Mitigation Acquire::http::AllowRedirect=false Acquire::Retries=0

2019 was a verification bypass: the check itself was fooled. 2026 is a time-of-check-to-time-of-use flaw: the check is correct and passes legitimately, and then the verified thing is swapped out from under it before it gets used. Different mechanisms, identical lesson about trusting the transport.

How the 2026 Swap Works

The primary advisory, DSA 6607-1, states it in two sentences:

APT can merge two different package downloads after an HTTP redirect gives them the same URI. Improper file handling when resuming an HTTP download allows a man-in-the-middle attacker to [replace a package after verification], resulting in the execution of arbitrary code as root.

Unpack that into the sequence it describes. apt downloads many files per run, and to avoid fetching the same thing twice it deduplicates: if two downloads resolve to the same URI, apt treats them as one file on disk, reusing it through a hard link — a second directory name for the exact same underlying inode. Two names, one file. Write through either name and both see the change, because there's only one file.

An attacker on the network path arranges, via a redirect, for two different package downloads to resolve to the same URI. apt hard-links them: the two logical downloads are now one physical file. One of those downloads is a legitimate package that apt fetches, verifies against the signed index, and marks as good. Then the attacker triggers the second condition — a transient HTTP error that forces apt to retry and resume the other download. On resume, apt writes the attacker's bytes into the file it is resuming. But that file is a hard link to the one it already verified. The verified package's inode now contains attacker-chosen content, and dpkg installs it, running its maintainer scripts as root.

The verification was real. It passed on the real package. The file it blessed just does not contain the real package any more by the time it is used. That's CWE-367 in one paragraph, and it's why NVD classifies it that way.

Two conditions gate it, and both are worth knowing:

  • Acquire::Retries must be non-zero. No retry, no resume, no second write. This has been the default since apt 2.3.2, which covers Debian 12 and Ubuntu 22.04 LTS and everything newer. Older releases are not exposed by default — but countless CI scripts and Dockerfiles set Acquire::Retries explicitly to survive flaky networks, and every one of those re-arms the bug on an otherwise-safe apt.
  • The attacker must sit on the network path. A man in the middle, a malicious proxy, or a hostile mirror. On plain-HTTP mirrors that is a realistic position; the signature was supposed to make it harmless.
We recommend reading What Is Supply-Chain Security? to continue reading our selection of content. apt is the most-trusted link in a Debian system's supply chain, and this is a failure in the link everything else assumes is sound.

What Is Supply-Chain Security? Why the integrity of how you fetch software matters as much as the software itself. SudoSecurity

The Scoring, and What AT:P and UI:P Are Telling You

Canonical, as CNA, scored it 7.7 High under CVSS 4.0 with this vector:

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Two metrics in there carry the real meaning and are easy to skim past. AT:P — Attack Requirements: Present — is CVSS 4.0's way of saying the attack is simple to execute but needs a condition the attacker does not fully control, here the man-in-the-middle position. UI:P — User Interaction: Passive — means a victim has to do something, but nothing unusual: run apt upgrade, the single most routine administrative action on a Debian system. The three Hs that follow (VC:H/VI:H/VA:H) are total compromise of the machine.

So the real reading of 7.7 is: if an attacker can get on your package traffic, root falls out of a completely normal apt upgrade, and the only thing holding the score below the nines is that getting on the traffic is a precondition rather than a given. On a laptop on a trusted network, that precondition is a real barrier. On a cloud instance pulling from a plain-HTTP mirror over a shared network, or a CI runner fetching packages through a proxy you do not fully control, it's thinner than it sounds.

The Fix Is One Idea: Stop Sharing the File

The upstream changelog entry is three words — "Open target files exclusively" — and the stable fix is commit 5af16a8 on the 3.0.y branch. The whole class of bug depends on two downloads sharing one inode, so the fix refuses to share them: each download opens its target file exclusively, and the hard-link reuse that made the swap possible can no longer happen. The advisory notes the same update also closed several out-of-bounds access issues in passing, which is common when a security release opens up a subsystem for scrutiny.

You can't easily demonstrate the full attack without a man-in-the-middle rig, and this article won't give you one. But the filesystem truth underneath it — that a hard link means a write through one name changes the "other" file — takes four commands and no apt at all:

$ echo "verified package contents" > verified.deb
$ ln verified.deb resumed.deb          # same inode, two names
$ sha256sum verified.deb               # "verification" passes here
e40e8f8b...  verified.deb
$ echo "attacker payload" > resumed.deb   # write through the OTHER name
$ cat verified.deb
attacker payload

The checksum you took in step three described a file that no longer exists by step five, even though its name never changed. apt's bug was letting an attacker control the equivalent of resumed.deb while it trusted verified.deb. The fix is to never let those two be the same inode in the first place.

We recommend reading npm Dependency Security Before CI: Where Trust Decisions Actually Happen to continue reading our selection of content. The trust boundary in a build is the moment you fetch something and decide it is safe — and that decision is exactly what a time-of-check-to-time-of-use bug attacks.

npm Dependency Security Before CI: Where Trust Decisions Actually Happen The trust boundary in a modern build is the dependency you inherited, not the code you wrote. SudoSecurity

What To Do

Patch. The fix shipped the same day across the archive, so for most people this is a normal update — with one wrinkle, which is that you're using the vulnerable tool to fix the vulnerable tool. Canonical's own guidance is to disable retries for the upgrade that installs the fix, which removes the precondition during the one window it matters:

sudo apt update
sudo apt upgrade -o Acquire::Retries=0

Confirm you landed on a fixed build with apt policy apt. The fixed versions by release:

Release Fixed apt version
Debian 13 (trixie) 3.0.4 (DSA-6607-1)
Debian 12 (bookworm) 2.6.2 (DLA-4880-1)
Debian testing / unstable 3.3.6
Ubuntu 26.10 3.3.3ubuntu0.1
Ubuntu 26.04 3.2.1ubuntu0.1
Ubuntu 24.04 LTS 2.8.4ubuntu0.1
Ubuntu 22.04 LTS 2.4.15ubuntu0.1

Ubuntu 20.04 and earlier are covered only through Ubuntu Pro / ESM. Debian 9–11 under Extended LTS got 1.4.12, 1.8.2.4 and 2.2.5 via Freexian.

Two structural things outlast this particular patch:

  • Use HTTPS mirrors. https://deb.debian.org/debian and the Ubuntu equivalents remove the network-path attacker from the board entirely, because an on-path adversary cannot inject the redirects and errors the attack needs into a TLS session. HTTPS doesn't help against a mirror that's itself malicious — the operator can do what they like — but it closes the man-in-the-middle case for both this bug and the 2019 one. Justicz's 2019 argument for HTTPS-by-default would have blunted the attack he filed in 2026. That is worth sitting with.
  • Audit Acquire::Retries in your images and CI. If you set it to survive flaky networks, you re-enabled the precondition on releases that would otherwise have been safe by default. That doesn't mean leave it at zero forever. It means patch, and know that the setting is security-relevant rather than purely operational.

This is also the second musl-and-minimalism lesson of the same week — the OpenSSH sshole bug was a C-library quirk under your containers; this is a verification gap in the tool that builds them. Both live below the line most threat models draw.

What This Doesn't Cover

I read the primary advisory (DSA 6607-1, via oss-security) and the CVE record, and I verified the hard-link aliasing behaviour that underlies the bug with the local demonstration above. I did not build a man-in-the-middle rig, reproduce the end-to-end package swap, or read the vulnerable apt code at the fix commit — the public Debian mirror on GitHub trails the real tree on salsa.debian.org, which I could not clone here, so the fix commit 5af16a8 and the "Open target files exclusively" change are quoted from the advisory and changelog rather than read line by line. The step-by-step merge-and-resume sequence is assembled from the advisory's two sentences, the CWE-367 classification, and the published analyses; the exact code path is not something I confirmed in source, and I have said so rather than dressing an inference as a reading.

No working exploit appears here and none is linked. The four-command hard-link demonstration is a filesystem fact, not an attack — it touches no package manager and no network.

Out of scope: the out-of-bounds issues bundled into the same release, the full Ubuntu ESM matrix, and the broader question of whether Debian should have moved to HTTPS-by-default in 2019. That last one is a real debate with reasonable positions on both sides, and this CVE is one more data point in it rather than a settlement of it.

The Actual Point

The thing that should bother you is not that apt had a bug. Everything has bugs. It's that the same researcher walked through the same program to the same root shell seven years apart, and both times the deeper cause was the same decision: trust a signed artifact so completely that the transport underneath it is treated as irrelevant.

In 2019 the transport handed apt a poisoned redirect and apt verified the poison. In 2026 the transport let apt verify the real thing and then swapped it. Each specific bug got fixed — redirects got hardened, files now open exclusively — but the posture that made both reachable is still standing: plain HTTP under a signature, on the theory that the signature is enough. A signature proves what a file was when it was checked. It says nothing about what the file is at the moment it is used, and the gap between those two tenses is where both of Max Justicz's apt bugs lived.

The one-line version for your infrastructure: patch apt today, and switch your mirrors to HTTPS so that the next time someone finds the third version of this bug, the man in the middle isn't even in the room. Defense in depth isn't a slogan here. It is the specific control that would have made two seven-years-apart root exploits into non-events.

The DSA 6607-1 advisory text and CVSS vector are quoted from the primary oss-security posting and the CVE record. The 2019 comparison is drawn from the CVE-2019-3462 reporting and Justicz's own account. The hard-link demonstration is real local shell output. The vulnerable apt code was not read at the fix commit; that limitation is stated above.

$ tail -f /forums

Discussion

Comments live on the community forums — sign in there to reply.