# Root Cause Analysis: CVE-2026-82208

## Summary

CVE-2026-82208 is a certificate-policy bypass in libcurl's wolfSSL TLS backend. When CA caching is enabled, a cache-hit connection can leave `wssl->x509_store_setup` false. libcurl calls `Curl_wssl_setup_x509_store()` before an application's `CURLOPT_SSL_CTX_FUNCTION` callback, the callback replaces the trust store with stricter application policy, and then libcurl calls setup again during the handshake. The vulnerable second call silently reinstalls the cached CA store, overriding the callback's policy. Current-run testing reproduced the bypass twice against the vulnerable fixed-commit parent and showed the fixed commit rejecting the same second transfer twice.

## Impact

- **Package/component affected:** curl/libcurl, specifically the wolfSSL TLS backend when CA caching and `CURLOPT_SSL_CTX_FUNCTION` trust-store customization are used.
- **Affected versions:** curl 8.9.1 through 8.21.0 inclusive. Versions before 8.9.1 and curl 8.22.0 or later are not affected.
- **Tested vulnerable commit:** `f8df560ed9faa2ff3d6198b50f55eaf0315fcb39`, the direct parent of the fixed commit.
- **Tested fixed commit:** `ed0338befd1d865a8ea1fbaa90013a096dedd07a`.
- **Risk level and consequences:** Upstream rates this Low severity (CWE-295). Under the required application configuration, an HTTPS peer certificate accepted by the cached CA store can be accepted even though application-installed certificate policy rejects it, undermining authentication and authorization decisions based on that policy. The curl command-line tool is not affected because this requires libcurl callback use.

## Impact Parity

- **Disclosed/claimed maximum impact:** Certificate-policy/authorization bypass (`authz_bypass`).
- **Reproduced impact from this run:** Full policy bypass. On each vulnerable run, the application callback installed an empty wolfSSL trust store, yet the second TLS handshake succeeded and received HTTP 200 from the peer. The fixed commit failed the same transfer with `CURLE_SSL_CACERT_BADFILE` (77) after reporting that the CA signer was unavailable.
- **Parity:** `full`.
- **Not demonstrated:** No code execution, memory corruption, or effect beyond bypassing the configured certificate trust policy was claimed or tested.

## Root Cause

The CA cache is held on the libcurl multi handle. In the vulnerable implementation, the line that marks setup complete—`wssl->x509_store_setup = TRUE`—is inside `wssl_populate_x509_store()`. A cache miss populates a new store and reaches that assignment. A cache hit, however, takes the `cached_store` branch in `Curl_wssl_setup_x509_store()` and returns without calling `wssl_populate_x509_store()`, so the completion flag remains false.

During a new wolfSSL connection, libcurl first checks this flag and installs the cached store before calling the application's `CURLOPT_SSL_CTX_FUNCTION` callback. The callback can replace the trust store to enforce stricter policy. Later in the handshake, libcurl checks the still-false flag again, invokes `Curl_wssl_setup_x509_store()` a second time, and restores the cached store. The callback did execute, but its trust decision state no longer controls certificate validation.

The fix moves `wssl->x509_store_setup = TRUE` to the beginning of `Curl_wssl_setup_x509_store()`, ensuring every path—including a cache hit—is marked complete and preventing the post-callback rerun:

- Fix: <https://github.com/curl/curl/commit/ed0338befd1d865a8ea1fbaa90013a096dedd07a>
- Introduced by the CA-cache/UAF correction: <https://github.com/curl/curl/commit/0f2876b2c33f6784a27b6f7345bd8cd95b46352a>

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` with `PRUVA_ROOT` optionally pointing to the bundle directory.
2. The script reads `bundle/project_cache_context.json`, uses `/pruva/project-cache/repo` when prepared, fetches fixed commit `ed0338befd1d865a8ea1fbaa90013a096dedd07a`, resolves its parent as the vulnerable checkout, and verifies the patch is absent/present in the expected function preamble.
3. It builds pinned wolfSSL 5.8.4 commit `59f4fa568615396fbf381b073b220d1e8d61e4c2` and static vulnerable/fixed libcurl builds with no default CA path. This makes the explicit CAfile the sole trust source and satisfies the wolfSSL CA-cache criteria.
4. It generates a local CA/server certificate and starts a real local HTTPS peer.
5. For each role, a C application performs one CAfile-authenticated transfer to populate the multi handle's CA cache, then a fresh second transfer with `CURLOPT_SSL_CTX_FUNCTION`. The callback replaces the wolfSSL X.509 store with an empty store, which must reject the server certificate if preserved.
6. The script runs two vulnerable and two fixed attempts. It requires vulnerable HTTP 200 acceptance, fixed certificate rejection, and exactly six HTTP requests reaching the peer (four vulnerable requests plus two fixed cache-priming requests).
7. It writes `bundle/repro/runtime_manifest.json` with immutable target identity and proof-artifact SHA-256 values.

Expected terminal output is:

```text
CVE-2026-82208 CONFIRMED: vulnerable CA-cache hit overrides callback-selected trust store; fixed commit fails closed.
```

## Evidence

- **Vulnerable attempts:**
  - `bundle/repro/vulnerable-attempt-1.log`
  - `bundle/repro/vulnerable-attempt-2.log`
- **Fixed negative controls:**
  - `bundle/repro/fixed-attempt-1.log`
  - `bundle/repro/fixed-attempt-2.log`
- **Real HTTPS peer transcript:** `bundle/repro/https-peer.log`
- **Exact source/dependency identity:** `bundle/repro/target-identity.log`
- **Machine-readable evidence binding:** `bundle/repro/runtime_manifest.json`
- **Build/session diagnostics:** `bundle/logs/reproduction_steps.log` and `bundle/logs/fix.patch`

Key vulnerable excerpt (present in both vulnerable logs):

```text
CALLBACK_EMPTY_STORE_INSTALLED
* SSL connection using TLSv1.3 / TLS13-AES256-GCM-SHA384
< HTTP/1.1 200 OK
SECOND_RESULT code=0 name=No error http=200
VULNERABILITY_CONFIRMED cached_CA_overrode_callback_policy
```

Key fixed excerpt (present in both fixed logs):

```text
CALLBACK_EMPTY_STORE_INSTALLED
*  CA signer not available for verification
SECOND_RESULT code=77 name=Problem with the SSL CA cert (path? access rights?) http=0
FIXED_NEGATIVE_CONTROL callback_policy_enforced
```

The final runtime manifest binds the tested vulnerable identity to SHA-256 `ebf927ca04aa4ea5794dbdcc4f97ce45f184a311d935b1fbf5e6195d4a1b9252`, computed from `git:https://github.com/curl/curl.git@f8df560ed9faa2ff3d6198b50f55eaf0315fcb39`. The observed environment was Linux x86-64, libcurl multi API, wolfSSL 5.8.4, and Python 3's TLS-capable local HTTPS peer. No sanitizer was used.

## Recommendations / Next Steps

- Upgrade to curl 8.22.0 or later.
- If immediate upgrade is unavailable, apply upstream commit `ed0338befd1d865a8ea1fbaa90013a096dedd07a` and rebuild libcurl.
- As a temporary mitigation, avoid combining the wolfSSL backend, CA caching, and `CURLOPT_SSL_CTX_FUNCTION` trust-store replacement.
- Retain a regression test that primes a CAfile-only multi-handle cache, installs a callback-selected rejecting store on a fresh transfer, and confirms the policy remains effective on the cache-hit path.
- Test cache-hit and cache-miss branches independently; cache misses already set the old completion flag and therefore do not expose the same sequence.

## Additional Notes

- The finalized script passed twice consecutively, and each script execution itself ran two vulnerable and two fixed attempts.
- The callback uses an empty trust store as a deterministic stricter policy. This models the advisory's callback-selected trust store and proves policy state is overridden without relying on application-specific certificate contents.
- The local HTTPS peer intentionally closes each HTTP connection. CA caching is independent of connection reuse; forcing a fresh TLS connection ensures the second connection reaches the vulnerable cache-hit setup sequence.
- A compiled-in default CA path initially prevented the cache criteria from being satisfied. The final build explicitly uses `--without-ca-bundle --without-ca-path`, and the runtime logs confirm `CApath: none`.
