All posts

Reproducing DEBIAN-CVE-2026-92390 (curl -J clobbered by 103 Early Hints)

curl / libcurl

Versions
Version typeVulnerableFixed
Releases7.20.0 - 8.22.08.23.0
Tested builds8.22.1-DEV8.23.1-DEV
Status
Catalog
Fixed in fork
Fork branchMerged
SeverityLowUpdated
Related links
Advisory
Official advisoryOSV recordCatalog record
Fixes
Upstream fixFork mergeFork branch
Code
Vulnerable sourceFixed sourceFork repository
Discussion
Original report and discussion

Setup

curl -J can overwrite an existing file when a 103 Early Hints response arrives before the final response.

Vulnerableupstream at b31c352, reports 8.22.1-DEV
Fixedour fork at ce57339, reports 8.23.1-DEV
Buildsame flags both sides
Sandboxno network, loopback only

With -J, an interim 103 response or an ETag under --etag-save could open the output file before curl decided the filename, truncating a pre-existing file. Upstream fix c24a69d8, included in the fork, buffers headers until the filename decision is complete and then refuses to overwrite (exit 23).

This reproduction starts a loopback server that answers /victim92390 with 103 Early Hints, followed by a final 200 with Content-Disposition: attachment. The client creates that file with the contents "original" and runs curl -O -J -i. If the contents change, the file was clobbered.

Reproduce (vulnerable build)

Command
docker run --rm --network none --pids-limit 64 --memory 512m --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m cve-92390-demo python3 /task/repro.py /opt/tree-vuln/bin/curl
Output
$ LD_LIBRARY_PATH=/opt/tree-vuln/lib /opt/tree-vuln/bin/curl --version | head -1
curl 8.22.1-DEV (x86_64-pc-linux-gnu) libcurl/8.22.1-DEV OpenSSL/3.5.7
$ LD_LIBRARY_PATH=/opt/tree-vuln/lib /opt/tree-vuln/bin/curl -O -J -i --output-dir /tmp/cj92390 --max-time 20 http://127.0.0.1:8000/victim92390
--- exit: 23
--- stdout:

--- file /tmp/cj92390/victim92390 after:
b'HTTP/1.1 103 Early Hints\r\nLink: </style.css>; rel=preload\r\n\r\n'
RED - pre-existing file clobbered

The pre-existing file now holds the 103 headers instead of "original". The 103 path opened the output before the filename decision.

The regression tests show the same failure.

Command
docker run --rm --network none --pids-limit 64 --memory 1g cve-92390-demo sh -c 'cp /src/fixed/tests/data/test3037 /src/fixed/tests/data/test3038 /src/vuln/tests/data/ && cd /src/vuln && make -C tests/server >/tmp/srv.log 2>&1 && cd tests && perl runtests.pl 3037 3038 2>&1 | tail -n 25'
Output
* Perl: v5.40.1 (/usr/bin/perl)
* Args: 3037 3038
* Seed: 266361
* Servers: HTTP-IPv6 HTTP-unix FTP-IPv6
*****************************************
Warning: test3037 not present in tests/data/Makefile.am
Warning: test3038 not present in tests/data/Makefile.am
test 3037 [Do not open output for 103 Early Hints with -J and --show-headers]

 3037: output (log/victim3037) FAILED:
--- log/check-expected
+++ log/check-generated
@@ -1 +1,2 @@
-original[LF]
+HTTP/1.1 103 Early Hints[LF]
+[LF]

 - abort tests
TESTDONE: 1 tests were considered during 1 seconds.
TESTDONE: 0 tests out of 1 reported OK: 0%

FAIL 3037: 'Do not open output for 103 Early Hints with -J and --show-headers' HTTP, HTTP GET, 103, Early Hints, -J, --show-headers

TESTFAIL: These test cases failed: 3037

The ETag test also overwrites the file.

Command
docker run --rm --network none --pids-limit 64 --memory 1g cve-92390-demo sh -c 'cp /src/fixed/tests/data/test3037 /src/fixed/tests/data/test3038 /src/vuln/tests/data/ && cd /src/vuln && make -C tests/server >/tmp/srv.log 2>&1 && cd tests && perl runtests.pl 3038 2>&1 | tail -n 22'
Output
* Args: 3038
* Seed: 266361
* Servers: HTTP-IPv6 HTTP-unix FTP-IPv6
*****************************************
Warning: test3038 not present in tests/data/Makefile.am
test 3038 [Do not open output for ETag with -J, --show-headers, --etag-save]

 3038: output (log/victim3038) FAILED:
--- log/check-expected
+++ log/check-generated
@@ -1 +1 @@
-original[LF]
+ETag: "tag3038"[LF]

 - abort tests
TESTDONE: 1 tests were considered during 1 seconds.
TESTDONE: 0 tests out of 1 reported OK: 0%

FAIL 3038: 'Do not open output for ETag with -J, --show-headers, --etag-save' HTTP, HTTP GET, ETag, --etag-save, -J, --show-headers

TESTFAIL: These test cases failed: 3038

Verify (fork-fixed build)

Run the same repro against the fork build.

Command
docker run --rm --network none --pids-limit 64 --memory 512m --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m cve-92390-demo python3 /task/repro.py /opt/tree-fixed/bin/curl
Output
$ LD_LIBRARY_PATH=/opt/tree-fixed/lib /opt/tree-fixed/bin/curl --version | head -1
curl 8.23.1-DEV (x86_64-pc-linux-gnu) libcurl/8.23.1-DEV OpenSSL/3.5.7
$ LD_LIBRARY_PATH=/opt/tree-fixed/lib /opt/tree-fixed/bin/curl -O -J -i --output-dir /tmp/cj92390 --max-time 20 http://127.0.0.1:8000/victim92390
--- exit: 23
--- stdout:

--- file /tmp/cj92390/victim92390 after:
b'original'
GREEN - pre-existing file intact

The file keeps its bytes. Exit 23 is the documented refusal to overwrite, not a crash. No download completes, and nothing is written.

Both regression tests pass on the fixed tree.

Command
docker run --rm --network none --pids-limit 64 --memory 1g cve-92390-demo sh -c 'cd /src/fixed && make -C tests/server >/tmp/srv.log 2>&1 && cd tests && perl runtests.pl 3037 3038 2>&1 | tail -n 10'
Output
* Servers: HTTP-IPv6 HTTP-unix FTP-IPv6
*****************************************
test 3037 [Do not open output for 103 Early Hints with -J and --show-headers]
--p---oe--- OK (1   out of 2  , remaining: 00:00, took 1.023s, duration: 00:01)
test 3038 [Do not open output for ETag with -J, --show-headers, --etag-save]
--p---oe--- OK (2   out of 2  , remaining: 00:00, took 0.037s, duration: 00:01)
TESTDONE: 2 tests were considered during 1 seconds.
TESTDONE: 2 tests out of 2 reported OK: 100%

Limits

The clobber needs -O -J -i (or --etag-save for the ETag path) plus a server that sends 103 Early Hints or a pre-CD ETag header. Without -i the header path differs and the file handling changes. The fixed build still exits 23 and downloads nothing when the target file exists. That is the intended no-overwrite behavior, kept from before.

Attack flow

Client pre-creates victim92390 holding "original"
Server answers 103 Early Hints, then 200 with Content-Disposition -J filename
curl -O -J -i processes the 103 headers first
Vulnerable: output file opened early, holds 103 headers (RED)
Fixed: headers buffered until filename decided, file untouched, exit 23 (GREEN)

Agent runbook

Agent runbook - run in order
docker run --rm --network none --pids-limit 64 --memory 512m --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m cve-92390-demo python3 /task/repro.py /opt/tree-vuln/bin/curl
docker run --rm --network none --pids-limit 64 --memory 1g cve-92390-demo sh -c 'cp /src/fixed/tests/data/test3037 /src/fixed/tests/data/test3038 /src/vuln/tests/data/ && cd /src/vuln && make -C tests/server >/tmp/srv.log 2>&1 && cd tests && perl runtests.pl 3037 3038 2>&1 | tail -n 25'
docker run --rm --network none --pids-limit 64 --memory 1g cve-92390-demo sh -c 'cp /src/fixed/tests/data/test3037 /src/fixed/tests/data/test3038 /src/vuln/tests/data/ && cd /src/vuln && make -C tests/server >/tmp/srv.log 2>&1 && cd tests && perl runtests.pl 3038 2>&1 | tail -n 22'
docker run --rm --network none --pids-limit 64 --memory 512m --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m cve-92390-demo python3 /task/repro.py /opt/tree-fixed/bin/curl
docker run --rm --network none --pids-limit 64 --memory 1g cve-92390-demo sh -c 'cd /src/fixed && make -C tests/server >/tmp/srv.log 2>&1 && cd tests && perl runtests.pl 3037 3038 2>&1 | tail -n 10'

Release information

SourceOfficial advisory
Checked
SeverityLow
WeaknessCWE-73
Other fixed branches
  • 8.20.x from 8.20.2
  • 8.16.x from 8.16.2
  • 8.14.x from 8.14.3

Recorded evidence

upstream fix present in fork base
regression test tests/data/test5033 executed RED then GREEN

Status history

DateStatusRecord
Fixed in forkmerged to fork master aab434c