SevyDevy

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.

CVE-2026-92392 at a glance (as of 19 October 2026)
FactDetail
Name in the advisoryHTTP/2 upload Use-After-Free
SeverityHigh, on curl's scale of low, medium, high and critical. curl does not publish CVSS scores.
WeaknessCWE-416, use after free
Affected versions8.12.0 up to and including 8.22.0, the library and the curl tool
Fixed in8.23.0, released 14 October 2026
Patched older lines8.14.3, 8.16.2 and 8.20.2, or later on those lines
WorkaroundDo 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

libcurl 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

The 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.

Where the team found libcurl, and who updates each copy
PlaceHow libcurl gets thereVersion stringWho must update it
Desktop billing appBundled in the installer8.15.0The team, by shipping a new app build
API container imageUbuntu 26.04 LTS curl package8.18.0-1ubuntu2.8Ubuntu, and this build already has the fix; the team confirms it in Ubuntu's notice
Reports servicepycurl 7.48.0, a Python wrapper, installed from a wheel8.22.0The pycurl maintainers, or the team by building pycurl itself
CI runner, the machine that builds and tests codeUbuntu 24.04 LTS curl package8.5.0-2ubuntu10.15Nobody 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.

bash
curl --version | head -n 2
python3 -c "import pycurl; print(pycurl.version)"
dpkg-query -W curl libcurl4t64

On 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.

python
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:

text
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 affected

The 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.

python
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, safely

It 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.

  1. Find every copy

    Search apps, container images, CI runners and language packages, including Python wheels. Ask each running program which libcurl it loaded.

  2. Rank by exposure

    Put uploads to user-supplied or third-party servers first. The team starts with the accountant upload in the desktop app.

  3. 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.

  4. 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.

  5. Chase your vendors

    For bundled software you did not build, such as pycurl wheels, ask the maintainer which release carries the fix.

  6. 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.

  1. Name the bug class

    A use-after-free: libcurl kept a pointer to a transfer the app freed, after an HTTP/2 stream reset.

  2. Give the trigger

    A malicious server pauses an upload, replies in part and resets the stream. The victim must be uploading to that server.

  3. Size the risk honestly

    Code execution needs eight narrow conditions; a crash is likelier. curl rates it high, not critical.

  4. Show the reach

    Keep a software bill of materials: a list of every library each product ships, including copies bundled inside other packages.

  5. 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

Practise this with Lala

Practise