Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

curl Security Update: Patch Every Copy in Your Stack (8.23.0, 2026)

The curl security update you should be installing right now is curl 8.23.0, released October 14, 2026. It fixes 24 security vulnerabilities, including one the curl project itself rates HIGH — its first High-severity flaw since the SOCKS5 heap overflow of October 2023. That flaw, tracked as CVE-2026-92392, lets a malicious server turn a routine HTTP/2 upload into remote code execution on the client. The kicker is not the bug itself. It is where curl lives: in your system package manager, your Docker base images, your CI runners, your PHP and Python stacks, and dozens of tools you have never audited. Patching one copy is not patching curl. This guide shows you how to find every copy and update it.

What this curl security update fixes

The headline vulnerability is CVE-2026-92392, an HTTP/2 upload use-after-free affecting curl and libcurl 8.12.0 through 8.22.0 — every release since February 2025. The remaining 23 advisories in this release are all rated low, and they cluster around familiar plumbing: credentials and connection reuse escaping their intended scope (Authorization Negotiate headers sent to foreign origins, host credentials leaking to an HTTP proxy, client certificates attached to the wrong reused connection), cookie-jar and HSTS edge cases, and protocol corner cases in FTP wildcards, TFTP, IMAP APPEND, and WebSockets under GnuTLS.

A few facts worth knowing before you act:

  • The trigger is server-side. The victim uploads a file of roughly 135KB over HTTP/2 to an attacker-controlled server whose certificate passes the client’s checks. libcurl negotiates HTTP/2 automatically for HTTPS URLs when the server offers it and the build has HTTP/2 enabled.
  • It was AI-found. The flaw was reported by Luna Tong at Anthropic using their new OSS Scanner. It landed on September 15 and was confirmed within hours — at which point the project shortened its release cycle from eight weeks to six to ship the fix sooner.
  • Exploitation is hard, not impossible. curl says real-world code execution needs exact knowledge of the victim’s curl build, memory layout, distro, libc, and TLS bundle — and that the most likely outcome of an attempted attack is a crash. That is why it is High, not Critical. There are no reports of it being exploited in the wild.
  • This release also closes an audit. 8.23.0 was curl’s fifth external official audit, and a large share of the 24 CVEs came out of an AISLE audit. Rock-solid curl builds were released in sync, and distros were alerted ahead of time via the distros@openwall list — so fixed packages are flowing through the normal channels right now.

How CVE-2026-92392 actually works (the plain-English version)

Understanding the mechanism matters because it explains why the fix is a version bump, not a config tweak. curl’s maintainer Daniel Stenberg published a detailed post-release write-up; here is the shape of it.

libcurl represents every transfer as an easy handle, and groups concurrent transfers under a multi handle. HTTP/2 is multiplexed, so curl is constantly juggling multiple logical transfers over one physical connection, each with a different lifetime.

The attack sequence goes like this: the client starts an upload over HTTP/2. The malicious server pauses the send by zeroing the HTTP/2 window, returns a piece of the response, then resets the stream — which kills the transfer but not the connection. curl correctly tears down the transfer, but accidentally keeps a lingering pointer to the freed easy handle. When the multi handle is cleaned up, libcurl misunderstands the situation, allocates download memory, finds the dangling pointer, and writes server-controlled data into the exact memory the easy handle used to occupy.

Two things then happen. First, the confused request leaks internal client details — including local addresses — which the server needs to defeat ASLR and build a working payload. Second, when libcurl processes the downloaded data through the dangling handle, it follows a function pointer the server’s data has just overwritten — and runs the attacker’s code.

The long-term fix, per the curl project, is to reduce the number of raw internal pointers to handles and instead store transfer identifiers that get looked up on demand — marginally slower, far more foolproof against this class of mistake — plus more tests and more static analysis. Multiplexed protocols make dangling pointers fatal; that is the lesson here, and it is why Stenberg says they keep iterating on the C codebase rather than attempting a rewrite.

Are you affected? Check every curl on your box

Start with the obvious one, then go looking for the ones you forgot about.

1. The system curl

curl --version

You want 8.23.0 or newer — but read the next section first, because distro packages complicate this. Check whether your build even has the vulnerable protocol enabled:

curl -V | grep -i http2

If HTTP2 appears in the Features line, HTTP/2 negotiation is on, which is the attack path. Now find the library behind the binary:

ldconfig -p | grep libcurl

Multiple libcurl versions on one box is common and entirely normal — and it is exactly the problem.

2. The language runtimes

PHP — curl is compiled into the interpreter or loaded as an extension. This will not track your system curl version:

php -r 'echo curl_version()["version"], PHP_EOL;'
# or check the loaded extension in phpinfo() output

Python — if you use pycurl rather than the pure-Python requests/httpx stack:

python -c "import pycurl; print(pycurl.version)"

Node.js — the popular node-libcurl binding ships its own bundled libcurl. Check the version bundled in your node_modules, not your system curl. Same story for Ruby’s curb gem and Rust’s curl crate: the version that matters is the one your dependency tree pulls in, which curl --version knows nothing about.

Where curl hides in your stack

This is the part most update guides skip. curl is infrastructure plumbing — it is everywhere precisely because nobody thinks about it. Walk this list:

  • System package manager — curl and libcurl4 on Debian/Ubuntu, curl/libcurl on RHEL/Fedora, curl on Alpine, Homebrew’s curl on macOS. Every VPS, every jump host, every workstation.
  • Docker base images — debian:bookworm-slim, ubuntu:24.04, alpine, and every language image built on top of them ship distro curl. A pinned old image is a pinned old curl. Your multi-stage builds inherit it too.
  • CI/CD runners — GitHub Actions, GitLab runners, and self-hosted agents use curl in setup scripts and composite actions. Check the runner image, not just your app image.
  • PHP builds — the curl extension links its own libcurl. On Ubuntu this usually follows the system library; on compiled-from-source PHP setups and Docker images built with docker-php-ext-install curl, it is whatever the build baked in.
  • Python, Node, Ruby, Rust apps — anything using pycurl, node-libcurl, curb, or the curl crate carries a bundled copy. Run pip show pycurl / check your lockfiles.
  • Statically linked binaries and vendored tools — some Go and Rust CLIs vendor a static libcurl. strings on a suspicious binary or checking the vendor’s release notes is the only way to know.
  • Things that curl for you — package managers, Terraform providers, deployment scripts, monitoring agents, and cron jobs that curl a healthcheck endpoint all exercise the vulnerable code path when they upload over HTTPS.

How to update curl everywhere it hides

Package managers (the easy ones)

Update the package, then verify. Note that distros backport security fixes — your Ubuntu LTS box will not show 8.23.0, it will show a patched 8.x build from your distro’s security repository, and that is correct.

# Debian / Ubuntu
sudo apt update && sudo apt upgrade curl libcurl4
curl --version

# Check what's waiting before you commit
apt list --upgradable 2>/dev/null | grep curl

# RHEL / Fedora / Rocky / AlmaLinux
sudo dnf update curl libcurl

# Alpine
apk upgrade curl

# macOS
brew upgrade curl

# Windows
winget upgrade curl

On long-running servers, remember that running processes keep using the old library until they restart. Any daemon or cron worker that holds libcurl open — PHP-FPM workers, queue workers, monitoring agents — needs a restart after the package upgrade for the fix to take effect.

Docker images

You cannot apt upgrade your way out of a baked image. Rebuild every image whose base layer contains curl, with no cache, and re-push:

docker build --no-cache -t myapp:latest .
docker push myapp:latest

If you pin digest or old tags in production, the rebuild does not help until you deploy the new tag. And if your CI builds the image, fix it there first — otherwise your next deploy quietly reintroduces the vulnerable copy.

Language runtimes and vendored copies

  • PHP — update the system libcurl, then restart PHP-FPM/Apache. For docker-php-ext-install curl images, rebuild the image.
  • Python pycurl — pip install --upgrade pycurl after updating system libcurl, since the wheel links against it; better still, rebuild the container.
  • Node / Ruby / Rust bindings — bump the binding version in your lockfile to one that vendors libcurl ≥ 8.23.0 (or a backported build), then npm install / bundle install / cargo update and redeploy.
  • Static/vendor binaries — watch the vendor’s release notes for a rebuild announcement. There is no command that fixes these; you replace the binary.

Building from source (the escape hatch)

If your distro has not shipped a fixed package yet and you cannot wait, build upstream 8.23.0 from the tarball on curl.se/download.html:

tar xzf curl-8.23.0.tar.gz
cd curl-8.23.0
./configure --with-openssl
make
sudo make install

This replaces the system curl, which package managers dislike. Prefer the distro package when it exists — source builds are for the gap between “vulnerability public” and “distro package ready”, which right now is measured in days.

Can’t apply the curl security update yet? Two stopgap mitigations

If a full rollout will take days — pinned production images, change freezes, third-party appliances — you can defuse the specific attack path without upgrading. The vulnerability only triggers over HTTP/2, so forcing HTTP/1.1 removes the trigger:

# curl CLI: pin a single invocation to HTTP/1.1
curl --http1.1 https://example.com/upload

For libcurl-based applications, the equivalent is setting the HTTP version option on the handle:

/* libcurl C API */
curl_easy_setopt(handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);

Scope this narrowly: pin HTTP/1.1 only on the uploads and integrations that touch untrusted servers, and treat it as a bridge to the real patch, not a substitute. It costs you HTTP/2 multiplexing performance, and it does nothing for the other 23 advisories.

Why this curl security update matters beyond the patch

Three things make 8.23.0 worth paying attention to even after you have upgraded.

First, AI is now finding the bugs. CVE-2026-92392 came out of Anthropic’s OSS Scanner — an AI-assisted analysis pipeline. The curl team is frank about the double edge: AI-assisted tooling helped surface the flaw, but human review and validation remained essential to handling it. Expect more of these, not fewer.

Second, audits work, but they arrive in waves. A large portion of this release’s 24 CVEs traces back to an external AISLE audit — curl’s fifth official external audit. Independent audits keep finding real issues in one of the most scrutinised codebases on the planet. If curl, which is continuously probed by “countless humans and AIs”, still ships a High every few years, your dependencies deserve the same scepticism.

Third, the memory-safety debate got an answer, of a sort. Stenberg is explicit: libcurl stays in C. A rewrite in Rust would be, in his words, a monstrous undertaking — and the existing team are not Rust experts, while libcurl runs on more platforms than Rust targets. The strategy is instead to make the C code structurally safer: fewer raw handle pointers, more looked-up identifiers, more tests, more analysers. You can disagree with the choice, but it is an honest engineering position, and the transfer-identifier fix in this release is that philosophy in action.

The 60-second checklist

  1. curl --version — is it 8.23.0+, or a distro-patched build released today?
  2. curl -V | grep -i http2 — is the HTTP/2 attack path even compiled in?
  3. Check phpinfo() / pycurl / node-libcurl / lockfiles for bundled copies.
  4. Rebuild and redeploy Docker images — no cache, new tag, actually deployed.
  5. Restart long-running services (PHP-FPM, queue workers, agents) so they load the new libcurl.
  6. Stuck? Pin --http1.1 on untrusted uploads as a stopgap, then schedule the real patch.

curl is the kind of dependency you update and forget — until the day the update matters. Today is that day. Go find every copy.

Further Reading & References

Leave a Comment