Guide · Backend
Curl's October 2026 HTTP/2 Use-After-Free Explained: Upgrade to 8.23.0
18 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The curl HTTP2 use after free bug, CVE-2026-92392 (its public security ID), lets a malicious server make libcurl use memory it already gave back. HTTP/2 is version 2 of HTTP, the web's request protocol. libcurl is the library inside curl that many apps build in. curl 8.23.0 fixes it.
- It affects curl and libcurl 8.12.0 to 8.22.0, except the patched older lines 8.14.3, 8.16.2 and 8.20.2. The curl project rates it high, its first high since CVE-2023-38545.
- During an HTTP/2 upload, the server pauses the upload, sends part of a reply, then cancels that one request. libcurl keeps a stale pointer, a saved memory address, to a transfer the app then frees.
- The reporter turned it into remote code execution, where an attacker runs their own code on your machine. That needs eight narrow conditions at once; the maintainers expect a crash instead.
- Beginners can stop after the remote code execution section. Engineers get the version checks. As of 19 October 2026, all 48 prebuilt packages (wheels) of pycurl 7.48.0, a Python wrapper for libcurl, bundle an affected libcurl. Ubuntu 26.04's fixed curl still says 8.18.0.
What goes wrong when a library frees memory but keeps a pointer to it?
The program can later read or write memory that now belongs to something else. That bug is called a use-after-free. The curl HTTP2 use after free flaw, fixed on 14 October 2026, is one of them. Its CVE, the public ID for a known security flaw, is CVE-2026-92392.
For most teams the hard part is not the bug itself. It is finding every copy of libcurl they run, because it hides inside apps, container images and language packages.
Picture a common setup. A small company sells a desktop billing app to shops. Every night the app uploads a backup of the day's sales over HTTPS. The setup is made up, but the bug is real.
Shops can also send a copy to their accountant's server by typing in its address. The team reads the curl advisory, the project's official notice about the flaw, and asks one question: are we affected?
Which words do you need before reading about the curl bug?
curl and libcurl. curl is a command-line tool that moves data to and from URLs. libcurl is the library underneath, which other programs build in. The billing app ships its own copy of libcurl.
Easy handle and multi handle. In libcurl, each transfer, such as one upload, is tracked by an easy handle: a block of memory holding that transfer's state. A multi handle runs many easy handles together.
Stream and multiplexing. HTTP/2, version 2 of the web's request protocol HTTP, can carry many requests over one connection. Each request and its response get their own stream, a numbered lane inside the connection. Sharing one connection like this is called multiplexing.
Frames, flow-control window and RST_STREAM. HTTP/2 sends data in small chunks called frames. The window is how many bytes the receiver will accept right now, so a window of zero means wait. RST_STREAM is a frame that cancels one stream but keeps the connection open.
Use-after-free. A program frees memory when it is done, so the memory can be reused. Using it later through an old pointer, a saved memory address, is a use-after-free. The standard weakness list, CWE, calls it CWE-416.
Think of a hotel room. The room is a block of memory, checking out is freeing it, and the key card you forgot to hand back is the stale pointer. A new guest moves in, and your card still opens their door.
Where the picture breaks: a hotel would change the lock code at checkout. C does nothing of the kind. Nothing stops an old pointer from working, so the program must remember to stop using it.
What is the curl HTTP2 use after free bug (CVE-2026-92392)?
It is a use-after-free in libcurl that a server triggers during an HTTP/2 upload. The server resets the upload's stream with one of two error codes, and libcurl later uses a transfer the application has freed.
Both error codes come from the HTTP/2 standard, RFC 9113. REFUSED_STREAM means the server refused the stream before doing any work on it. HTTP_1_1_REQUIRED means the server wants the client to use HTTP/1.1 instead.
You do not have to opt in to HTTP/2 to be exposed. The maintainer's write-up says libcurl negotiates HTTP/2 by default for HTTPS URLs when the server agrees.
| Fact | Detail |
|---|---|
| Name in the advisory | HTTP/2 upload Use-After-Free |
| Severity | High, on curl's scale of low, medium, high and critical. curl does not publish CVSS scores. |
| Weakness | CWE-416, use after free |
| Affected versions | 8.12.0 up to and including 8.22.0, the library and the curl tool |
| Fixed in | 8.23.0, released 14 October 2026 |
| Patched older lines | 8.14.3, 8.16.2 and 8.20.2, or later on those lines |
| Workaround | Do not use HTTP/2 until you upgrade |
CVSS is a common 0 to 10 severity score. curl's disclosure policy says the project does not support it, so any CVSS number you see came from someone else.
How does HTTP/2 multiplexing carry many transfers on one connection?
HTTP/2 splits one connection into many streams, one per request. Frames from different streams are mixed together, like lorries from different firms sharing one highway. Streams a client opens get odd numbers: 1, 3, 5 and so on.
Inside libcurl, each stream belongs to one easy handle, and the multi handle keeps the shared connections. When a transfer ends, every link from the connection to that easy handle must go.
One HTTP/2 connection, many transfers
Drag to explorelibcurl tracks which easy handle owns each stream on the shared connection. The bug left one of those links pointing at a handle the app had already freed.
Diagram connections:
- Billing app to Multi handle (adds transfers)
- Multi handle to Easy handle: sales backup
- Multi handle to Easy handle: second transfer
- Easy handle: sales backup to One HTTP/2 connection (stream 1)
- Easy handle: second transfer to One HTTP/2 connection (stream 3)
- One HTTP/2 connection to Remote server (frames mixed together)
- Billing app: Starts each transfer through libcurl
- Multi handle: Runs all transfers and keeps the connections
- Easy handle: sales backup: State for the nightly backup upload
- Easy handle: second transfer: Another upload or download running alongside
- One HTTP/2 connection: Encrypted connection carrying many streams
- Remote server: Answers each stream and can pause or reset one
You can watch the two rules the bug depends on, without curl. On 19 October 2026 we ran a short in-memory test with the Python h2 library 4.4.1, which implements the HTTP/2 rules.
A REFUSED_STREAM reset on stream 1 ended that stream only; stream 3 kept carrying data. Stream 3's window started at 65,535 bytes, the HTTP/2 default. Once it reached 0, sending one more byte raised FlowControlError.
How does an HTTP/2 stream reset leave curl with a stale pointer?
A malicious server walks libcurl through a short sequence that ends with a stale pointer. The maintainer's write-up gives the order shown below, and the advisory names the reset codes.
Pause and predict: the server cancels the upload's stream and libcurl ends the transfer cleanly. The app then frees the transfer. Which part of libcurl could still be holding on to it?
The trigger sequence, step by step
Drag to exploreThe first four steps look like normal error handling. The damage starts with the pointer libcurl keeps in step five.
Diagram connections:
- Upload starts to Window set to zero (pauses upload)
- Window set to zero to Part of the response (sends partial reply)
- Part of the response to Stream reset (resets stream)
- Stream reset to Transfer pulled down (keeps pointer)
- Transfer pulled down to App frees easy handle (frees handle)
- App frees easy handle to Multi handle cleanup (follows pointer)
- Upload starts: Client → server: backup data over HTTP/2
- Window set to zero: Server → client: stop sending for now
- Part of the response: Server → client: a piece of the reply
- Stream reset: Server → client: RST_STREAM, connection stays open
- Transfer pulled down: libcurl → itself: ends it but keeps a handle pointer
- App frees easy handle: App → libcurl: done with this transfer
- Multi handle cleanup: libcurl → freed memory: the use-after-free
The answer is the connection side. libcurl ends the failed transfer but, in the write-up's words, leaves a lingering pointer to the easy handle. The app sees the failure and frees the handle, as it should.
Then the app cleans up the multi handle. libcurl follows the leftover pointer into memory it no longer owns. That is the hotel key card opening the new guest's door.
What just happened? Every step looked like normal error handling. The bug was one forgotten link between the connection and a transfer that no longer existed.
Who could play the malicious server? In the billing app, the accountant feature is the risky one: a shopkeeper can type in any address, including one the company does not control.
Can the curl use-after-free really lead to remote code execution?
Yes, the reporter showed it can, but only when many conditions line up at once. The curl team rated the bug high rather than critical and estimates the chance of a successful attack as slim.
The write-up explains the idea without the recipe. The server's data lands in the memory where the freed easy handle used to live. libcurl then calls a function through that overwritten handle.
The advisory lists eight conditions an attacker needs, all at once. In plain words:
- The exact curl version and the layout of its internal structures, to work out where things sit in memory.
- The exact processor architecture of the machine running curl.
- A known, compatible C library memory allocator and a predictable heap, the area where programs get memory at run time.
- Specific behaviour of the HTTP/2 library nghttp2 and of the encryption library OpenSSL with TLS 1.3, including memory used for session tickets. TLS is the protocol that encrypts HTTPS, and session tickets are saved data for resuming TLS connections.
- A pinned CA bundle, the fixed list of certificate issuers curl trusts, and other inputs that shape memory use.
- Two transfers running in parallel, with their timing kept just right.
- Correctly calibrated addresses inside the C library, learned from a leak.
- A command short enough to fit in about 23 bytes.
The write-up adds two more: the victim must upload to the attacker's server, and the upload must be about 135 KB. Its conclusion is that the most likely outcome of an attempted attack is a crash.
A crash still hurts. For the billing app, it means a shop loses that night's backup.
If you are new to this, you can stop here. The rest is for engineers who must find, check and patch every copy of libcurl.
Why does a bug in libcurl reach so many apps?
Because libcurl is a building block, not only a tool. Many programs load it as a shared library, a file the operating system loads at run time. Others bundle their own private copy.
Upgrading the curl command does not fix an app with its own libcurl. Whoever built each copy must update it, which is why the team starts with a list.
The team finds four copies. The desktop app's version is made up for the example. The other three are real version strings we checked on 19 October 2026.
| Place | How libcurl gets there | Version string | Who must update it |
|---|---|---|---|
| Desktop billing app | Bundled in the installer | 8.15.0 | The team, by shipping a new app build |
| API container image | Ubuntu 26.04 LTS curl package | 8.18.0-1ubuntu2.8 | Ubuntu, and this build already has the fix; the team confirms it in Ubuntu's notice |
| Reports service | pycurl 7.48.0, a Python wrapper, installed from a wheel | 8.22.0 | The pycurl maintainers, or the team by building pycurl itself |
| CI runner, the machine that builds and tests code | Ubuntu 24.04 LTS curl package | 8.5.0-2ubuntu10.15 | Nobody for this bug: 8.5.0 predates 8.12.0 |
How do you check which libcurl version you really run?
Ask the program that runs, not the package list. The curl tool and pycurl both report the libcurl version they actually loaded. On Debian or Ubuntu, dpkg-query shows the distribution's package version.
curl --version | head -n 2
python3 -c "import pycurl; print(pycurl.version)"
dpkg-query -W curl libcurl4t64On our Ubuntu 24.04 runner, the first command printed curl 8.5.0 and libcurl/8.5.0. Its second line read: Release-Date: 2023-12-06, security patched: 8.5.0-2ubuntu10.15.
The pycurl line printed PycURL/7.48.0 libcurl/8.22.0, followed by the TLS and HTTP/2 libraries. dpkg-query printed curl 8.5.0-2ubuntu10.15 and libcurl4t64:amd64 8.5.0-2ubuntu10.15.
On Ubuntu 22.04 the library package is still called libcurl4, not libcurl4t64. There, run dpkg-query -W curl libcurl4 instead.
pycurl wheels still carry the affected libcurl
A wheel is the prebuilt package pip downloads. As of 19 October 2026, pycurl 7.48.0 is the newest release on PyPI, uploaded on 16 September 2026.
We opened all 48 of its wheels. The 40 for Linux, Windows and Apple Silicon Macs bundle libcurl 8.22.0. The eight for Intel Macs bundle libcurl 8.21.0. Both versions are inside the affected range, and upgrading your system curl does not touch these copies.
A script that sorts the versions
The team writes a small script for the list. It knows the affected range and the three patched older lines from the advisory. It flags distribution builds for a manual check instead of guessing.
import re
FIRST, FIXED = (8, 12, 0), (8, 23, 0)
# Older release lines that got their own fix, per the advisory
BACKPORT_FIXES = {(8, 14): 3, (8, 16): 2, (8, 20): 2}
def verdict(version):
m = re.match(r"(\d+)\.(\d+)\.(\d+)(.*)", version)
v, extra = tuple(int(x) for x in m.groups()[:3]), m.group(4)
if not FIRST <= v < FIXED:
return "not affected"
fixed_patch = BACKPORT_FIXES.get(v[:2])
if fixed_patch is not None and v[2] >= fixed_patch:
return "not affected (patched older line)"
if extra:
return "in range: check the distro's notice"
return "affected: upgrade to 8.23.0"
copies = {
"Desktop app (bundled)": "8.15.0",
"API image (Ubuntu 26.04)": "8.18.0-1ubuntu2.8",
"Reports service (pycurl wheel)": "8.22.0",
"CI runner (Ubuntu 24.04)": "8.5.0-2ubuntu10.15",
}
for place, version in copies.items():
print(f"{place}: {version} -> {verdict(version)}")Run with Python 3.13, it prints these four lines:
Desktop app (bundled): 8.15.0 -> affected: upgrade to 8.23.0
API image (Ubuntu 26.04): 8.18.0-1ubuntu2.8 -> in range: check the distro's notice
Reports service (pycurl wheel): 8.22.0 -> affected: upgrade to 8.23.0
CI runner (Ubuntu 24.04): 8.5.0-2ubuntu10.15 -> not affectedThe desktop app and the reports service come out affected. The Ubuntu 26.04 image is in range by number, so it is flagged for a check. The CI runner is not affected.
Ubuntu's page for this CVE, as of 19 October 2026, lists 26.04 LTS as fixed in that package. It lists 24.04, 22.04 and older LTS releases as not affected.
How did curl 8.23.0 fix the use-after-free?
The fix is a small reorder in how libcurl finishes a transfer. The commit the advisory links is titled "multi: adjust the cleanup order in multi_done". It moves two calls in one file, lib/multi.c: six lines added and six removed.
Connection filters are the layers, such as TLS and HTTP/2, that sit on a connection. A comment added by the patch says they must drop their references to the transfer at that point.
The write-up spends more time on the long-term defence. Over the last few years, curl has been cutting internal pointers to handles. It stores transfer IDs, plain numbers, and looks each transfer up when needed.
A transfer that is gone gives an empty lookup, not freed memory. The maintainers call this a tiny bit slower but more foolproof against logical mistakes like this one.
Here is the same idea in Python. Python cannot produce a real use-after-free, so this shows only the lookup pattern.
import itertools
transfers = {} # transfer ID -> state of one upload
next_id = itertools.count(1)
def start_upload(name):
tid = next(next_id)
transfers[tid] = {"name": name, "bytes_sent": 0}
return tid
def on_stream_reset(tid):
transfer = transfers.get(tid)
if transfer is None:
print(f"Transfer {tid} is already gone, nothing to touch")
return
print(f"Stopping upload {transfer['name']}")
tid = start_upload("nightly-backup")
transfers.pop(tid) # the app is done and frees the transfer
on_stream_reset(tid) # a late event finds nothing, safelyIt prints: Transfer 1 is already gone, nothing to touch. In C, a raw pointer to freed memory gives no such warning; it just reads whatever is there now.
The write-up says this is no guarantee against future use-after-free bugs. The advisory calls the bug a C mistake that would likely have been avoided had curl not used C. The write-up rules out a rewrite and promises more tests and analysers.
Why did curl 8.23.0 ship two weeks early in October 2026?
Because of this report. The write-up says the team shortened the release cycle from eight weeks to six. The release post confirms 42 days since the previous release.
curl's vulnerability list, as of 19 October 2026, shows 24 advisories published on 14 October 2026. This is the only high one; the other 23 are rated low.
Not all 24 touch every version. curl's page for 8.22.0 lists 21 flaws in it: this high one and 20 low ones. Upgrading from 8.22.0 fixes all 21.
curl's disclosure policy gives Linux distributions up to seven days of private notice before a release. Ubuntu's page for this CVE is dated 14 October 2026, the release day.
How do you patch curl and every copy of libcurl?
Move every affected copy to 8.23.0 or a patched build, starting with code that uploads to servers you do not control. The team followed these steps.
Find every copy
Search apps, container images, CI runners and language packages, including Python wheels. Ask each running program which libcurl it loaded.
Rank by exposure
Put uploads to user-supplied or third-party servers first. The team starts with the accountant upload in the desktop app.
Upgrade or rebuild
Ship 8.23.0 in bundled copies, rebuild images on a patched base, or apply the advisory's patch to your own builds.
Use the workaround while you wait
Avoid HTTP/2 for those uploads. Use --http1.1 with the curl tool, or set CURLOPT_HTTP_VERSION to CURL_HTTP_VERSION_1_1 in libcurl.
Chase your vendors
For bundled software you did not build, such as pycurl wheels, ask the maintainer which release carries the fix.
Verify and record
Run the version checks again after deploying. Expect libcurl/8.23.0 or a patched distribution build, and keep the list for the next advisory.
For the reports service, the team sets the HTTP/1.1 option on its pycurl handles until a fixed wheel exists. We checked that pycurl 7.48.0 accepts that option.
How do you explain the curl use-after-free in a system design interview?
Treat it as a story about object lifetimes and dependencies. Use this frame.
Name the bug class
A use-after-free: libcurl kept a pointer to a transfer the app freed, after an HTTP/2 stream reset.
Give the trigger
A malicious server pauses an upload, replies in part and resets the stream. The victim must be uploading to that server.
Size the risk honestly
Code execution needs eight narrow conditions; a crash is likelier. curl rates it high, not critical.
Show the reach
Keep a software bill of materials: a list of every library each product ships, including copies bundled inside other packages.
Describe the fix as a pattern
Store IDs and look objects up when needed instead of keeping raw pointers to things another part can free.
Questions people ask
- Is the curl command-line tool affected by CVE-2026-92392?
- Yes. The advisory says the flaw also affects the curl command-line tool, in versions 8.12.0 to 8.22.0. The trigger is an HTTP/2 upload to a malicious server. Upgrading the tool does not fix programs that bundle their own libcurl, such as pycurl wheels.
- Do I need to worry if I only download files with curl?
- The advisory describes the flaw during an upload, and the write-up's trigger is an upload. Neither says downloads are safe, as of 19 October 2026. Upgrade anyway: curl's list shows 20 other low-severity flaws in 8.22.0 that 8.23.0 also fixes.
- Why is the curl bug rated high and not critical?
- Because a working attack needs many conditions at once. The advisory lists eight, from the exact curl build and allocator to two transfers with the right timing. The maintainer's write-up says such requirements are extremely difficult to meet in the wild. curl does not publish CVSS scores.
- Can I turn off HTTP/2 instead of upgrading?
- Only as a short stopgap. The advisory's third recommendation is not to use HTTP/2. Use --http1.1 with the tool, or CURLOPT_HTTP_VERSION set to CURL_HTTP_VERSION_1_1 in libcurl. You lose multiplexing, and the faulty code stays in your build until you upgrade.
- My Ubuntu server says curl 8.18.0. Is it affected?
- Check the full package version, not just 8.18.0. As of 19 October 2026, Ubuntu lists 26.04 LTS as fixed in 8.18.0-1ubuntu2.8, a backported fix. Run dpkg-query -W curl and compare. Ubuntu lists 24.04 and older LTS releases as not affected.
- Would a memory-safe language have prevented this bug?
- Probably. The advisory calls it a C mistake that would likely have been avoided had curl not used C. A memory-safe language checks memory use for you, and the write-up says one could probably have avoided it. The maintainer's write-up says curl will not be rewritten. It keeps replacing raw pointers with ID lookups and adding tests and analysers instead.
Sources and further reading
- curl: CVE-2026-92392, HTTP/2 upload Use-After-Free (security advisory)
- Daniel Stenberg: A UAF turned RCE (14 October 2026)
- curl fix commit: multi: adjust the cleanup order in multi_done
- Daniel Stenberg: curl 8.23.0 release post (14 October 2026)
- curl: Vulnerabilities in curl 8.22.0
- curl: all published vulnerabilities with severities (JSON)
- Daniel Stenberg: Twenty-two pending curl vulnerabilities (7 October 2026)
- curl: vulnerability disclosure policy (severity levels, CVSS, distros notice)
- libcurl: CURLOPT_HTTP_VERSION
- Ubuntu: CVE-2026-92392 status by release
- Debian security FAQ: why version numbers look vulnerable after backports
- PyPI: pycurl 7.48.0 (release date; the bundled libcurl version is from our own wheel check)
- RFC 9113: HTTP/2 (streams, flow control and error codes)