requests 2.32 SSLContext regression: the wrong mTLS client certificate
Requests 2.32.0 shared one SSLContext across verified connections. In a replay of the PR before merge, Warpway flags the client-cert mix-up later reported as #6726.
Published 3 min readReplayed through Warpway on By the Warpway team
Replay vs. what happened
Warpway replay
Flags it, given only what existed before merge
First user report (#6717)
7 days after merge
Flagged: Client certificates leaking across requests through one shared SSLContext
Replayed on 2026-10-07. Warpway was not installed on psf/requests when this pull request was open.
In May 2024, psf/requests#6667 changed how Requests verifies TLS certificates. Instead of loading the default CA bundle for each new connection, it loads the bundle once, at import, into a single SSLContext, and hands that object to every connection made with verify=True. The goal was sound: reloading root certificates is slow, especially on Windows with OpenSSL 3. The diff was small, one file, and it shipped in Requests 2.32.0.
On 2026-10-07 we replayed the pull request through Warpway as it stood before merge, without the review discussion or anything that happened later. In that replay, Warpway's Correctness lens reports two high-severity problems. Users reported both after release, and a year later the change was reverted.
Key facts
- Pull request
- psf/requests#6667
- Merged
- 2024-05-15
- Shipped in
- 2.32.0
- Affected
- 2.32.0 to 2.32.4
- Resolved in
- 2.32.5 (revert)
- Language
- Python
What Warpway flags in the replay
- CorrectnessIssue found
- SecurityIncomplete
- Testing / ReliabilityIssue found
- Architecture / MaintainabilityIssue found
- Backward CompatibilityIncomplete
- PerformanceCleared
- HighCorrectnesssrc/requests/adapters.py:94
Shared SSL context can reuse a client certificate on requests without one
For verify=True, every pool in every adapter receives the same module-global SSLContext. A request with cert=(certificate, key) also passes those files to urllib3; its TLS wrapping loads that client certificate into the supplied context. A later verify=True connection, including one from another Session or to another host, receives that same context even when cert=None. If the later server requests client authentication, it can receive the earlier request's client identity, violating the per-request cert setting and potentially disclosing/authenticating as the wrong client. Separate pool keys do not isolate mutable state inside the context.
- HighCorrectnesssrc/requests/adapters.py:75
Loading the default CA bundle at import blocks unrelated Requests usage
If the default certifi/distribution CA path is absent or unreadable, the new module-level load_verify_locations raises while adapters.py is imported. Because importing requests imports Session and HTTPAdapter, even HTTP requests, verify=False requests, and requests with an explicitly valid custom CA bundle cannot start. Previously the default bundle was extracted and validated only inside cert_verify for a verified HTTPS request without a custom path; other modes remained usable.
- HighTesting / Reliabilitysrc/requests/adapters.py:75
Verified connections retain roots removed from the default CA bundle
The default CA bundle is loaded only when adapters.py is imported. If a running process replaces its default bundle to remove a compromised root, a newly opened verify=True connection still uses the old in-memory trust store. Before this change, cert_verify supplied the bundle path and urllib3 loaded it when making new TLS connections. Thus an endpoint chaining to the removed root may continue to pass verification until process restart.
Warpway saw the pull request as it stood before merge: final code, title, description and commits, without review comments or later history. Findings are quoted verbatim from the recorded review. Several lenses can report the same issue; Warpway merges them, and the first paragraph is shown.
The change
_preloaded_ssl_context = create_urllib3_context()
_preloaded_ssl_context.load_verify_locations(
extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
)
# when building the connection pool for a request
elif verify is True:
pool_kwargs["ssl_context"] = _preloaded_ssl_contextHow the bug works
An SSLContext is not just configuration that gets read. urllib3 also writes to it: when a request passes cert=(certificate, key) for mutual TLS, urllib3 loads that client certificate into the context it was given. Before 2.32.0, each connection pool built its own context, so that write stayed local. From 2.32.0, every verify=True connection in the process receives the same object. A certificate loaded for one request is now present for others: requests from another Session, to another host, or with no cert= at all.
With concurrent requests using different client certificates, as in #6726, the shared context is modified while other connections are using it. The reporter's reproducer fails with an exception, and they first noticed the problem as the wrong certificates being presented.
The second finding has the same root. Loading the bundle moved from the verified-request path to module import, so anything wrong with the default bundle now breaks import requests for everyone, including programs that only make plain HTTP requests or pass their own CA bundle.
Are you affected?
You are affected if you run Requests 2.32.0 to 2.32.4 and any of these apply:
- You use client certificates (
cert=) withverify=True, especially several different certificates in one process or across threads. - You verify servers signed by a private or corporate root CA that is not in the default bundle.
- Your default CA bundle is extracted to a shared location that other users cannot read.
Fix or workaround
Upgrade to Requests 2.32.5 or later, which removes the shared default context.
If you cannot upgrade yet, pass a CA bundle path instead of True. With a string, Requests 2.32.x builds the connection from ca_certs and never uses the shared context:
import certifi
import requests
session = requests.Session()
session.cert = ("client.pem", "client.key")
session.verify = certifi.where() # a path, not TrueWhat happened next
Opened
#6667 opened
Merged
Release
Requests 2.32.0 released with the shared context
Reported
Release
Requests 2.32.5 ships the revert
Warpway replay
Warpway replay: given the pull request as it stood before merge, two high-severity Correctness findings
Frequently asked questions
Which versions of requests are affected?
Requests 2.32.0 through 2.32.4 reuse one preloaded SSLContext for every connection made with verify=True. Version 2.32.5 reverted the change.
Why would requests send the wrong client certificate?
When a request passes cert=, urllib3 loads that certificate into the SSLContext it is given. In 2.32.0 to 2.32.4 that context is shared by every verified connection in the process, so a certificate loaded for one request can be presented on another connection, including one made with a different certificate or none.
How do I avoid it without upgrading?
Pass a CA bundle path instead of True, for example verify=certifi.where(). With a string path, requests 2.32.x builds the connection from ca_certs and does not use the shared context. Upgrading to 2.32.5 or later is the simpler fix.
Did Warpway review this pull request at the time?
No. Warpway was not installed on Requests in 2024. We replayed the pull request on 2026-10-07, giving it only what existed before merge: the final code, title, description and commits, and the repository at the fork point. No review comments, bug reports or later history.
How we ran this
- 01
Replay the pull request as it stood before merge
Its final code, title, description and commits, and the repository at the fork point. No review comments, no bug reports, nothing after merge.
- 02
Review with Warpway’s default lenses
The production role models, lenses and default policy, run through Codex subscription access. One run per pull request, with no repository configuration.
- 03
Compare with what actually happened
Later fixes, reverts and bug reports in the public history, linked from every page so you can check our reading.
This pull request is one of 12 we replayed on 2026-10-07 from hono, axios and requests, including ones that were never fixed or reverted and the cases Warpway got wrong. Warpway was not installed on these repositories at the time; every review here is a replay. Older pull requests may appear in the model's training data, which is one reason we include recent ones and a bug nobody had reported. See every result from the pilot.
These replays used Codex subscription access rather than the production OpenAI API. They establish the recorded findings, not production API cost, general accuracy, customer time savings or an approval rate. The projects are open-source examples, not Warpway customers.
More case studies
- honojs/hono #5366TypeScript
Hono formData() after text() silently corrupts binary file uploads
- Warpway flags in a replay
- Binary file uploads silently altered when text() is read first
- What happened
- Not previously reported; we filed it as honojs/hono#5539
- honojs/hono #5133TypeScript
Hono cloneRawRequest: stale Content-Length after multipart re-encoding
- Warpway flags in a replay
- Forwarded multipart clones keep a Content-Length that no longer matches the body
- What happened
- Found independently 36 days after merge; fixed in 4.13.4
- honojs/hono #4707TypeScript
Hono 4.12.0: c.json(undefined) throws "Value is not JSON serializable"
- Warpway flags in a replay
- c.json(undefined) throws on the new fast path instead of returning a response
- What happened
- Reported 4 days after merge; fast path reverted in 4.12.2