Skip to main content

Docker's CVE-2026-34040 Is the Second Time This AuthZ Bypass Shipped: A Padded HTTP Request Still Grants Host Access on Unpatched Engines Below 29.3.1

8 min readDora NodaDora Noda
Share
On this page

One HTTP request, padded past 1MB, is all it takes to make a Docker AuthZ plugin approve a container it should have blocked — while the daemon quietly builds that exact container, root-mounted onto the host filesystem, anyway. That's CVE-2026-34040 (CVSS 8.8), disclosed in April 2026 and fixed in Docker Engine 29.3.1. It is not a new vulnerability class. It is the second time this specific bypass has shipped: the same daemon-to-plugin desync that produced 2024's maximum-severity CVE-2024-41110 (CVSS 10.0), patched, and quietly reopened under a different trigger.

The Payload That Gets Through

Here is the concrete mechanic, not the abstraction. A Docker AuthZ plugin — OPA, Prisma Cloud, Casbin, or a custom policy service — sits between the Docker API and the daemon, inspecting each request body and returning allow/deny. An operator relying on one might have a policy that flatly denies any Privileged: true container, or any bind mount of the host root. That policy is sound. The bypass is upstream of it.

The request an attacker sends is an ordinary POST /containers/create, with a JSON body like:

json
{
  "Image": "alpine",
  "HostConfig": {
    "Privileged": true,
    "Binds": ["/:/host"]
  },
  "_pad": "AAAA...(padded past 1MB)..."
}

Docker Engine's middleware caps the request body it forwards to the AuthZ plugin. When that body exceeds roughly 1MB, the middleware silently truncates what the plugin receives to empty — but keeps processing the full, original body itself. The plugin sees nothing, so it has nothing to deny, and returns allow. The daemon, meanwhile, executes the complete request it actually received: a privileged container with the entire host filesystem bind-mounted at /host. One shell into that container and the attacker has root-equivalent access to AWS credentials, SSH keys, Kubernetes kubeconfigs, and anything else living on the node — with the policy engine having "approved" a request it never saw.

No exploit tooling is required. It's a single crafted HTTP call, padded with junk bytes, that anyone who's read the Docker Engine API docs can construct by hand.

The underlying parsing gap this exploits isn't new to 2026 — researchers tracing the code path have found the same body/plugin size-mismatch behavior present as far back as Docker Engine 1.10, meaning the defect has been latent in the codebase for close to a decade. CVE-2026-34040 is simply the first time someone found a reproduction of it that the 2024 patch didn't already close.

The First Time: A Different Trigger, the Same Gap

CVE-2024-41110 hit the same seam two years earlier, from the opposite direction. Instead of an oversized body, the trigger was Content-Length: 0. Docker's forwarding logic checked r.ContentLength > 0 before deciding whether to send the body to the plugin; a zero-length header meant the plugin was called with no body at all, while the daemon — which reads the request differently — still processed the full payload attached below that header. Same daemon-executes-more-than-plugin-sees defect, different lever to trip it.

That bug was actually older than its 2024 CVE number suggests: it had already been found and fixed once, in Docker Engine 18.09.1, back in January 2019 — then silently reintroduced in the 19.03 line and left unpatched there through 27.1.0, a regression that sat live for roughly five years before anyone noticed a second time. When it resurfaced publicly in mid-2024, it drew a perfect 10.0 CVSS score, because unlike the 2026 sequel, exploiting it required no padding or size games — just one header.

Same Root Cause, Two Reproductions, One Missed Class

CVE-2024-41110CVE-2026-34040
TriggerContent-Length: 0 headerBody padded past ~1MB
What the plugin seesNo body at allBody silently truncated to empty
What the daemon executesThe full attached requestThe full original request
CVSS10.0 (Critical)8.8 (High)
First discovered2018 (fixed in 18.09.1); regressed, rediscovered 2024Regression of the 2024 fix; disclosed April 2026
Fixed inDocker Engine ≥ 19.03.15 patch line / ≥ 27.1.1 (per-branch backports)Docker Engine 29.3.1
Fix scopeRejected the specific zero-length-header reproduction(2024 fix) rejected the header case only — did not close the general body/plugin desync

Line up the two rows on "what the daemon executes" against "what the plugin sees" and the pattern is exact: both bugs are the same invariant violation — Docker never guaranteed that the bytes an AuthZ plugin evaluates are the same bytes the daemon acts on. The 2024 patch closed the one path that had a working proof of concept at the time (a zero Content-Length header) without touching the general question of what happens when body validation and body forwarding disagree under any condition. Two years later, an oversized body proved the general gap was still open. This is why the finding matters beyond "patch again": the fix addressed a reproduction, not the underlying parsing contract, and nothing about the 2026 patch guarantees a third trigger doesn't exist.

Why "We Have an AuthZ Plugin" Was Never the Safety Net

The detail that should reorder a self-hosted platform's threat model: this bypass is invisible to the plugin by construction. OPA, Prisma Cloud, Casbin, and every custom AuthZ service on the Docker plugin API are all downstream of the same daemon middleware, so a stricter policy, a better Rego rule, or a more paranoid custom plugin changes nothing — the plugin is denying a request it was never shown. If your platform's security model is "we don't run untrusted workloads as root because the AuthZ plugin blocks privileged containers," that control was silently off for every Docker Engine release before 29.3.1, regardless of which plugin you deployed or how it was configured.

For a self-hosted PaaS or a Cluster API-managed fleet running arbitrary tenant builds, that's the exposure that matters: multi-tenant build/deploy paths that hand a container spec to the Docker API on a shared node are exactly the surface this targets, and the AuthZ layer many such platforms lean on for isolation was not a backstop here — it was bypassed identically to having no plugin at all.

Detection and Mitigation, in Order

  1. Check the version, not the changelog. Run docker version on every node; anything reporting an Engine version below 29.3.1 is exploitable regardless of which AuthZ plugin is installed or how it's configured.
  2. Patch the engine — this is not plugin-configurable. The fix lives in Docker Engine's request-handling middleware, not in any AuthZ plugin's policy. No Rego rule, Casbin policy, or Prisma Cloud rule set closes this; only upgrading to 29.3.1+ does.
  3. Add user namespace remapping as defense-in-depth. With userns-remap enabled, container UID 0 maps to an unprivileged host UID — so even a container that slips through as Privileged: true doesn't hand out literal host root. Rootless Docker goes further, running the daemon itself without root. Note the tradeoff: Docker-in-Docker workflows generally need real root and are incompatible with user namespace remapping, so audit for that pattern before enabling it fleet-wide.
  4. Stop treating "has an AuthZ plugin" as a completed control. Log and alert on request bodies near or past your plugin's size ceiling — a legitimate container spec rarely approaches 1MB, so padded requests are a meaningful detection signal even before the version check catches up everywhere.
  5. Restrict Docker API exposure regardless of patch status. Both this bug and its 2024 predecessor assume the attacker can already reach the Docker API — locally or over an insecurely exposed TCP socket. Keeping the API off the network entirely, or behind mutual TLS and network policy, remains the single control that stops either bypass from mattering, patched or not.

The Patch-Tracking Lesson for Cluster API Fleets

The operational takeaway is less about this specific CVE pair and more about what kind of trust a "fixed" status deserves going forward. A Cluster API-managed fleet's upgrade tooling typically tracks CVEs as a checklist: CVE fixed in version X, node image bumped to X, item closed. That model treated CVE-2024-41110 as permanently resolved in mid-2024 — reasonably, since the specific reproduction stopped working. It wasn't wrong about the reproduction. It was wrong to assume the reproduction was the whole vulnerability class.

The concrete change worth making: when a CVE fix addresses a specific reproduction of a broader defect class (a parsing gap, a validation mismatch, a race condition) rather than the defect class itself, keep a regression test that replays the original exploit request against each new node image — not just a version-number check. For this bug specifically, that means a CI step that fires both the zero-Content-Length request and the oversized-body request at a test AuthZ plugin and asserts the plugin actually receives and denies the payload the daemon is about to act on.

A version bump proves you took the patch. A replayed exploit proves the class is actually closed. Given that this same daemon/plugin desync has now produced two CVEs from two different triggers, treating "patched once" as "closed forever" is exactly the assumption that let two years pass before anyone checked for a third way in.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, on a Cluster API-managed fleet with patch and node-image discipline built into the platform rather than bolted on after the fact. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex