HexBrain

curl · Report #3972316

46: `--libcurl` output carries `--insecure` across `--next` boundaries

Informativestatus
3upvotes
28 Aug 2026disclosed

How it was found

Summary

When --libcurl generates C source for a command that uses --next to separate multiple operations, local options from earlier operations leak into later ones. Specifically, if the first operation uses --insecure and the second does not, the generated program silently skips TLS certificate and hostname verification for the second operation. The curl CLI itself correctly rejects the untrusted certificate for the second operation (exit 60); the compiled generated program accepts it (exit 0).

Root cause

easysrc_init() emits one curl_easy_init() before all operations. easysrc_perform() emits curl_easy_perform() per operation. No curl_easy_reset() is ever emitted between operations.

Operation 1 emits:

curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);

For operation 2, tool_setopt_long() compares the secure values (1) against the defaults listed in setopt_nv_CURLNONZERODEFAULTS and suppresses both setters because the value matches the default. This optimization is correct for a fresh handle but wrong for the reused handle: libcurl options are sticky until explicitly changed or reset.

The result is that the generated source has one curl_easy_init(), two curl_easy_perform() calls, zero curl_easy_reset() calls, and the verify-disable setopts appear only once — before the first perform. The second perform inherits disabled verification.

Reproduction

Expected output:

TARGET_COMMIT=c2676bf9e639e31e44b12a2cc82cf2046231b056
CLI_MULTI_RC=60
GENERATED_MULTI_RC=0
SECURE_CLI_RC=60
SECURE_GENERATED_RC=60
GENERATED_COUNTS=init=1 perform=2 reset=0 peer_zero=1 host_zero=1
RESULT=VULNERABLE

Key observations:

  • CLI_MULTI_RC=60: curl itself correctly rejects the self-signed cert on the second (secure) operation.
  • GENERATED_MULTI_RC=0: the compiled generated program accepts it — TLS verification leaked from operation 1.
  • SECURE_*_RC=60: a secure-only generated program correctly rejects the cert (control).
  • GENERATED_COUNTS: one init, two performs, zero resets, verification disabled once — confirming no inter-operation reset.

Suggested fix

Emit curl_easy_reset() between generated operations (before each curl_easy_perform() after the first), then reapply global options that --next is documented to preserve. Alternatively, emit a separate curl_easy_init() / curl_easy_cleanup() per operation. The default-suppression logic in tool_setopt_long() should also be revisited for multi-operation correctness, or the reset should make it a non-issue. Other local options beyond --insecure are likely affected by the same sticky-state carryover and should be audited.

Impact

The --libcurl documentation states the output "does the equivalent of what your command-line operation does." The --next documentation states it "resets all local options." The generated code violates both promises in a security-critical dimension: TLS verification is silently disabled for operations that never requested it.

An application author who generates, compiles, and deploys this code creates a program where a network attacker can impersonate the later HTTPS endpoint without a valid certificate, even though that operation was intended to be secure.

The prerequisites — using --libcurl with --next, combining --insecure on only some operations, compiling and deploying the output — limit reach. Curl's own transfers are unaffected.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original