# How apt proves a package is genuine, and how to check and fix CVE-2026-109518

URL: https://systemdesignsimple.com/apt-package-signature-flaw-check-and-fix/
Published: 2026-11-07
Updated: 2026-11-06
Category: Security
Publisher: System Design Simple (https://systemdesignsimple.com)

## Short answer

CVE-2026-109518 lets an attacker controlling APT's HTTP responses replace a package after verification when retries are enabled. Update apt to your release's fixed version or later, then confirm the installed version with dpkg --compare-versions.

## Key facts

- **CVE:** CVE-2026-109518, found by Max Justicz, published 2026-11-04
- **Effect:** a package swapped after verification could be installed
- **Precondition:** on-path HTTP attacker, redirects and retries, Acquire::Retries above 0 (default 3 since apt 2.3.2)
- **Debian first fixed:** trixie 3.0.4 (DSA-6607-1), bookworm 2.6.2 (DLA-4880-1), forky and sid 3.3.6
- **Ubuntu first fixed:** 26.04 3.2.1ubuntu0.1, 24.04 2.8.4ubuntu0.1, 22.04 2.4.15ubuntu0.1 (USN-8996-1)
- **Signatures:** the InRelease signature check itself was not broken
- **Our lab:** 6 tampered repos: unsafe verifier accepted 6, fixed verifier rejected 6

If a Debian or Ubuntu machine runs an apt older than the first fixed package for its release, a party that can change its HTTP traffic could make apt install a package that never passed verification. The flaw is CVE-2026-109518, a vulnerability that enables a man-in-the-middle attack through how apt reuses downloaded files; the fix is to update apt and confirm the installed version is the listed one or later.

This guide is for protecting applications you own or maintain.

Debian published [DSA-6607-1](https://lists.debian.org/debian-security-announce/2026/msg00526.html) and Ubuntu published [USN-8996-1](https://ubuntu.com/security/notices/USN-8996-1) on 2026-11-04, and [The Hacker News](https://thehackernews.com/2026/11/debian-and-ubuntu-apt-flaw-lets-mitm.html) covered it the same day. Neither advisory reports exploitation.

## How apt checks that a package is genuine

apt does not check a signature on each `.deb`. It checks one signed metadata file and follows hashes down from it ([apt-secure](https://manpages.debian.org/trixie/apt/apt-secure.8.en.html)). A hash here is a SHA256 fingerprint of a file's bytes: change one byte and the fingerprint changes. The archive key is the OpenPGP key the repository owner signs that metadata with.

```text
# chain of trust, top to bottom
InRelease      signed Release metadata: SHA256 of every index, Date, optional Valid-Until
               (or Release plus a detached Release.gpg signature)
  └─ Packages  index; lists Filename and SHA256 of every .deb
       └─ hello_1.0.deb   accepted only if its SHA256 matches the Packages entry
```

With authentication and freshness checks enabled, apt rejects a bad signature, an index whose hash is not in the signed block, a `.deb` whose hash is not in the index, and metadata past its `Valid-Until`. Every link vouches for exact bytes. The guarantee covers the file that was hashed, so anything that later changes what sits behind that file name falls outside it.

## How CVE-2026-109518 swapped a verified package

The signature and hash checks worked. According to the [APT team](https://lists.debian.org/deity/2026/11/msg00005.html) and [DSA-6607-1](https://lists.debian.org/debian-security-announce/2026/msg00526.html), a party answering apt's HTTP requests could send redirects so that two different package downloads pointed at the same URI, plus transient errors so apt retried. apt merged the two downloads using existing hard links in its download directory, and a chosen payload could end up behind a file that had already passed its hash check.

The hard link is the detail that matters. A file's bytes live in an inode, and a file name is only a pointer to that inode. Two hard links are two names for one inode. Writing through one name changes the bytes you read through the other, and neither path is replaced or renamed, so a check that only watches paths sees nothing move. A verified name and an unverified name that share an inode are the same file.

APT's [upstream patch](https://salsa.debian.org/apt-team/apt/-/commit/5af16a8ad0f155233dfb10b7c92ad1903112ef79) opens download targets exclusively. When resuming, it retains a read handle to the old file, unlinks that destination, creates a fresh file, and copies the partial contents into it. Later writes then affect the new inode.

### Optional: the same gap in an updater you own

Application code that verifies and then installs from a path has the same check-then-use gap. The fragment below is illustrative and is not APT's code:

```js
// install.mjs (unsafe: hashes one read of the path, installs a later read)
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
const sha256 = (b) => createHash('sha256').update(b).digest('hex');

if (sha256(readFileSync(path)) !== expected) throw new Error('hash mismatch');
execFileSync('dpkg', ['-i', path]); // a write through another hard link, or a swap, lands here
```

```js
// install.mjs (fixed, illustrative fragment: install exactly the bytes that were verified)
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const sha256 = (b) => createHash('sha256').update(b).digest('hex');

const bytes = readFileSync(path);
if (sha256(bytes) !== expected) throw new Error('hash mismatch');
const dir = mkdtempSync(join(tmpdir(), 'verified-'));     // new directory, mode 0700
const safe = join(dir, 'pkg.deb');
writeFileSync(safe, bytes, { flag: 'wx', mode: 0o600 });  // new inode; fails if the name exists
execFileSync('dpkg', ['-i', safe]);
```

This example assumes other processes with the same privileges are trusted. Directory permissions do not isolate those processes from each other.

## Which apt versions are affected?

| Release | First fixed apt package | Record |
|---|---|---|
| Debian 13 trixie | 3.0.4 | DSA-6607-1 |
| Debian 12 bookworm | 2.6.2 | DLA-4880-1 |
| Debian forky, sid | 3.3.6 | security tracker |
| Ubuntu 26.10 | 3.3.3ubuntu0.1 | USN-8996-1 |
| Ubuntu 26.04 LTS | 3.2.1ubuntu0.1 | USN-8996-1 |
| Ubuntu 24.04 LTS | 2.8.4ubuntu0.1 | USN-8996-1 |
| Ubuntu 22.04 LTS | 2.4.15ubuntu0.1 | USN-8996-1 |
| Ubuntu 20.04, 18.04 | 2.0.12+esm1, 1.6.19+esm1 | Ubuntu Pro |

You are fixed when the installed version is the listed version or a later version for the same release. The bug also needs apt to retry downloads. According to the [APT team announcement](https://lists.debian.org/deity/2026/11/msg00005.html), `Acquire::Retries` has defaulted to 3 since apt 2.3.2, which covers Debian 12 and Ubuntu 22.04 onward. Older releases are exposed only if a config file or a command line turned retries on.

## Is my apt repo config safe?

Updating fixes the CVE. Per-repository settings decide how much of the chain is switched on ([sources.list](https://manpages.debian.org/trixie/apt/sources.list.5.en.html)):

```text
# /etc/apt/sources.list.d/vendor.list (unsafe)
deb [trusted=yes check-valid-until=no] http://repo.example.com/debian stable main
# and the vendor key dropped into /etc/apt/trusted.gpg.d/
```

```text
# /etc/apt/sources.list.d/vendor.sources (fixed)
Types: deb
URIs: https://repo.example.com/debian
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/vendor.asc
```

`trusted=yes` (deb822: `Trusted: yes`) accepts an unsigned or badly signed repo. `check-valid-until=no` (deb822: `Check-Valid-Until: no`) accepts an old index after it expires, which can hold back security updates. A key in `trusted.gpg.d` is global: it can authenticate any repository that has no narrower key selection. `Signed-By` limits a source to the key or keys it names.

## How to update apt

```sh
# Debian / Ubuntu
sudo apt update && sudo apt install --only-upgrade apt
# Ubuntu 24.04 threshold shown; use the first fixed version for your release from the table
installed=$(dpkg-query -W -f='${Version}' apt) || exit 1
dpkg --compare-versions "$installed" ge '2.8.4ubuntu0.1' || {
  echo "not fixed: $installed" >&2
  exit 1
}
apt changelog apt | grep CVE-2026-109518   # supporting detail only
```

`apt-cache policy apt` shows versions but does not compare them; `dpkg --compare-versions` applies Debian's version ordering. A changelog entry alone does not prove which package is installed.

The update itself travels over apt. Our own suggestion, not from the advisories: run it from a network you trust, against an HTTPS mirror if your release offers one. If you must delay, `Acquire::Retries "0";` in `/etc/apt/apt.conf.d/` temporarily removes one precondition the tracker documents. It only holds if the real invocation keeps that value, so check scripts for `-o Acquire::Retries=...`, and remove it after updating, since flaky mirrors will fail more often.

## What our lab showed

Our lab builds a throwaway signed repo with the same three layers (a signed InRelease, a Packages index and two package stand-in files that contain plain text), tampers with it six ways, and runs one verifier twice: every check on, then exactly one check off. It uses Ed25519 in a custom signature envelope in place of OpenPGP, installs nothing and runs offline. Run it with `node labs/security/apt-signature-lab/apt-signature-lab.mjs`. We ran it on this Mac on 2026-11-06 with Node.js v22.13.1.

[Image: Terminal output of our apt-signature-lab lab run: node run.mjs | The saved output of `node labs/security/apt-signature-lab/apt-signature-lab.mjs`, placed in a terminal-style page and captured in Chrome.]

Both verifiers accepted the untouched repo. Of the six tampered repos, the unsafe verifier accepted all six and the fixed one rejected all six: a `.deb` changed after signing (`REJECT hello_1.0.deb`), a `.deb` and Packages changed together (`REJECT index hash`), text added outside the signed block (`REJECT index hash`), a repo signed by another trusted key (`REJECT wrong key`), an expired index replayed (`REJECT expired`) and a removed signature (`REJECT unsigned`). The read-only version check found apt not installed on this Mac.

These six cases demonstrate signature scope, hash validation, key selection and expiry checks. They do not test the handoff from verification to installation, so they do not reproduce CVE-2026-109518.

Our take: update apt first. Repository hardening remains useful because it controls which keys and metadata your machine accepts.

## Fix checklist

| Step | Done when |
|---|---|
| Update apt | installed is the listed version or a later version for the same release (`dpkg --compare-versions ... ge`) |
| Old releases | Ubuntu 20.04 and older on Ubuntu Pro packages, or an upgrade planned |
| Until updated | `Acquire::Retries "0"` in config and no `-o Acquire::Retries=` override in scripts |
| Unsigned repos | no `trusted=yes`, `Trusted: yes` or `allow-insecure` anywhere |
| Key pinning | every third-party source has `Signed-By`; `trusted.gpg.d` holds no vendor keys |
| Freshness | expiry checks enabled for active repositories; documented exceptions for historical archives; consider `Valid-Until-Max` where metadata omits expiry |
| Your own updaters | install from the verified bytes in a private file, never by re-reading the path |

## Test your own app

Search your hosts and images for settings that switch a link off, in both `.list` and deb822 `.sources` syntax:

```sh
grep -rniE "trusted=yes|Trusted: *yes|allow-insecure|check-valid-until=no|Check-Valid-Until: *no|AllowUnauthenticated" /etc/apt/ Dockerfile* 2>/dev/null
grep -rnE "Acquire::(Retries|Check-Valid-Until)" /etc/apt/apt.conf.d/
grep -rnE "Acquire::Retries=" scripts/ .github/ Dockerfile* 2>/dev/null   # per-invocation overrides
ls /etc/apt/trusted.gpg.d/
apt-config dump | grep -iE "retries|check-valid-until"
```

In your own code, search for a hash check followed by a second read of the same path, for example `sha256(readFileSync(path))` and then `exec(..., path)`. A unit test can prove the fixed shape: verify a file, create a hard link to it, write new bytes through the link before install, and assert that the installed bytes are the verified ones. In CI, run the version check above inside your base image; it exits non-zero when apt is older than the threshold, so the build fails. Our [PHP security checklist](/php-security-common-vulnerabilities/), [Node.js security guide](/nodejs-security-common-vulnerabilities/) and [Python security guide](/python-security-common-vulnerabilities/) use the same search, test and fix approach for application code.

## Frequently asked questions

**Does HTTPS protect me from CVE-2026-109518?**
The advisories describe an attacker who can change HTTP traffic and do not name HTTPS as a mitigation. HTTPS to a mirror you trust makes on-path changes much harder, but a hostile mirror or proxy is still in scope. Update apt.

**My Ubuntu 24.04 shows apt 2.8.4ubuntu0.2. Is that fixed?**
Yes. Any version equal to or later than 2.8.4ubuntu0.1 on 24.04 contains the fix. Check with dpkg --compare-versions against the USN entry for your release, not against Debian's numbers.

**Was this exploited in the wild?**
Neither the Debian nor the Ubuntu advisory reports exploitation, as of 2026-11-06. Installing packages normally requires root privileges, so update anyway.

## Sources

- [Debian Security Advisory DSA-6607-1 apt, 2026-11-04](https://lists.debian.org/debian-security-announce/2026/msg00526.html)
- [Debian security tracker: CVE-2026-109518](https://security-tracker.debian.org/tracker/CVE-2026-109518)
- [APT team announcement on the deity list, 2026-11](https://lists.debian.org/deity/2026/11/msg00005.html)
- [Ubuntu Security Notice USN-8996-1: APT vulnerability, 2026-11-04](https://ubuntu.com/security/notices/USN-8996-1)
- [apt-secure(8) manual: archive authentication](https://manpages.debian.org/trixie/apt/apt-secure.8.en.html)
- [sources.list(5) manual: Signed-By, Check-Valid-Until, trusted](https://manpages.debian.org/trixie/apt/sources.list.5.en.html)
- [The Hacker News, 2026-11-04: Debian and Ubuntu APT flaw](https://thehackernews.com/2026/11/debian-and-ubuntu-apt-flaw-lets-mitm.html)
