🔒 Security view
Verified security findings — no false positives · Security Engineer, CISO, Pen Tester
⚡ Agent trace · 13 steps · $0.0458 · click to expand
search_claims(query='authentication authorization', category='security') →
search_claims(query='password secret credential', category='security') →
search_claims(query='injection validation sanitize', category='security') →
search_claims(query='auth token session', category='any') →
search_claims(query='password hash encrypt', category='any') →
search_claims(query='input validate escape', category='any') →
search_claims(query='security', category='security') →
search_claims(query='a', category='any') →
search_claims(query='ssl tls certificate verify', category='any') →
search_claims(query='cookie session redirect', category='any') →
search_claims(query='proxy header request', category='any') →
get_file_claims(file_path='src/requests/sessions.py') →
get_file_claims(file_path='src/requests/adapters.py') →
get_file_claims(file_path='src/requests/auth.py') →
read_source_file(file_path='src/requests/adapters.py', start_line=85) →
read_source_file(file_path='src/requests/adapters.py', start_line=307) →
read_source_file(file_path='src/requests/adapters.py', start_line=613) →
read_source_file(file_path='src/requests/adapters.py', start_line=673) →
read_source_file(file_path='src/requests/auth.py', start_line=34) →
read_source_file(file_path='src/requests/sessions.py', start_line=134) →
read_source_file(file_path='src/requests/auth.py', start_line=157) →
read_source_file(file_path='src/requests/auth.py', start_line=217) →
read_source_file(file_path='src/requests/sessions.py', start_line=194) →
read_source_file(file_path='src/requests/sessions.py', start_line=254) →
read_source_file(file_path='src/requests/auth.py', start_line=273) →
read_source_file(file_path='src/requests/sessions.py', start_line=309) →
read_source_file(file_path='src/requests/sessions.py', start_line=831) →
read_source_file(file_path='src/requests/sessions.py', start_line=752) →
get_file_claims(file_path='src/requests/models.py') →
search_claims(query='PreparedRequest header', category='any')
Security Review: Python Requests Library
HIGH — Exploitable Issues
1. SSL/TLS Certificate Verification Disabled by Default in cert_verify for Non-HTTPS URLs
What the code actually does:
In src/requests/adapters.py, the cert_verify method (lines 307–363) only performs certificate verification when the URL starts with "https" AND verify is truthy. If either condition is false — specifically if the URL is HTTPS but verify is falsy, or if the URL is HTTP — it sets conn.cert_reqs = "CERT_NONE" and clears CA certs (lines 343–346):
if url.lower().startswith("https") and verify:
# ... sets cert_reqs = "CERT_REQUIRED" ...
else:
conn.cert_reqs = "CERT_NONE"
conn.ca_certs = None
conn.ca_cert_dir = None
Under what conditions it becomes a risk:
This is by design for HTTP URLs (no TLS), but the risk is that verify=False is silently accepted for HTTPS URLs. The real exploitable issue is that there is no warning or error when a user explicitly passes verify=False. This is a well-known design choice in Requests, but it means any application that passes verify=False (e.g., to bypass self-signed certs in testing) is vulnerable to MITM attacks in production if that code path is accidentally left in.
Evidence: src/requests/adapters.py:321-346
Recommended fix: This is a library-level design decision. Applications should never use verify=False in production. The library could emit a SecurityWarning when verify=False is used, similar to what urllib3 does.
2. Proxy-Authorization Header Leakage on HTTPS Tunnels
What the code actually does:
In src/requests/sessions.py, the rebuild_proxies method (lines 334–368) adds a Proxy-Authorization header only when the scheme is NOT HTTPS (line 365):
if not scheme.startswith("https") and username and password:
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
However, in src/requests/adapters.py, the proxy_headers method (lines 613–632) always returns Proxy-Authorization headers for any proxy URL that contains credentials, regardless of scheme:
username, password = get_auth_from_url(proxy)
if username:
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
Under what conditions it becomes a risk:
When using a proxy with credentials over HTTPS, the proxy_headers method in the adapter will include the Proxy-Authorization header. For HTTPS CONNECT tunnels, this header is sent in the initial CONNECT request to the proxy (which is correct), but the rebuild_proxies method in the session layer deliberately avoids adding it for HTTPS to prevent leakage into the tunneled request. The adapter's proxy_headers method does not have this protection — it returns credentials for any proxy URL. If a custom adapter or a different code path calls proxy_headers directly, credentials could leak into the tunneled HTTPS request body/headers.
Evidence: src/requests/adapters.py:613-632, src/requests/sessions.py:355-366
Recommended fix: The proxy_headers method in the adapter should also check the scheme and avoid returning credentials for HTTPS tunneled requests, or the session layer should ensure the adapter's proxy_headers is not called for HTTPS proxies.
3. Digest Authentication Nonce Replay and Weak cnonce Generation
What the code actually does:
In src/requests/auth.py, the build_digest_header method (lines 157–266) generates a client nonce (cnonce) using:
s = str(self._thread_local.nonce_count).encode("utf-8")
s += nonce.encode("utf-8")
s += time.ctime().encode("utf-8")
s += os.urandom(8)
cnonce = hashlib.sha1(s, usedforsecurity=False).hexdigest()[:16]
The nonce_count is a simple integer counter that resets to 1 when the server nonce changes (lines 227–230). The time.ctime() call uses local time with second granularity. Only 8 bytes of os.urandom are used.
Under what conditions it becomes a risk:
The cnonce is only 16 hex characters (64 bits of entropy from the random portion). The use of time.ctime() (second granularity) and a predictable counter reduces entropy. An attacker who can observe multiple digest authentication exchanges can predict future cnonces with moderate effort. Additionally, the nonce_count is reset to 1 on redirects (line 271 in handle_redirect), enabling replay attacks across redirect chains.
Evidence: src/requests/auth.py:227-237, src/requests/auth.py:268-271
Recommended fix: Increase os.urandom to at least 16 bytes. Remove time.ctime() from the cnonce seed (it adds negligible entropy). Do not reset nonce_count on redirects — use a persistent counter per-session.
MEDIUM — Real Risks Worth Fixing
4. Authorization Header Not Stripped on Cross-Scheme Redirects (Same Host)
What the code actually does:
In src/requests/sessions.py, the should_strip_auth method (lines 154–184) determines whether to strip the Authorization header on redirect. It has a special case (lines 164–170) that allows http -> https redirects on standard ports to keep the Authorization header:
if (
old_parsed.scheme == "http"
and old_parsed.port in (80, None)
and new_parsed.scheme == "https"
and new_parsed.port in (443, None)
):
return False
However, the reverse (https -> http) is not explicitly allowed, but the logic at lines 173–184 only strips auth if the port or scheme changes. An https://example.com -> http://example.com redirect (same host, different scheme) would have changed_scheme = True, so auth would be stripped. But an https://example.com:443 -> https://example.com redirect (explicit to default port) would NOT strip auth because of the default port check (lines 175–181).
Under what conditions it becomes a risk:
If a server redirects from https://example.com:443 to https://example.com (dropping the explicit port), the Authorization header is preserved. This is technically correct (same host, same scheme), but the complexity of the logic means subtle bugs in custom redirect handling could leak credentials. The http -> https exception is a deliberate backwards-compatibility choice that violates RFC 7235 (which says auth should be stripped on cross-origin redirects).
Evidence: src/requests/sessions.py:154-184
Recommended fix: Consider removing the http -> https exception in a future major version. Document the behavior clearly.
5. Environment Variable CA Bundle Override Without Validation
What the code actually does:
In src/requests/sessions.py, the merge_environment_settings method (lines 831–868) reads REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE environment variables and uses them to override the verify parameter:
if verify is True or verify is None:
verify = (
os.environ.get("REQUESTS_CA_BUNDLE")
or os.environ.get("CURL_CA_BUNDLE")
or verify
)
Under what conditions it becomes a risk:
If an attacker can control environment variables (e.g., in a shared hosting environment, or via a compromised subprocess), they can point REQUESTS_CA_BUNDLE to a malicious CA bundle, effectively disabling certificate validation for all Requests calls made by that process. There is no validation that the path points to a legitimate CA bundle.
Evidence: src/requests/sessions.py:855-860
Recommended fix: At minimum, verify that the file exists and is a valid CA bundle before using it. Consider logging a warning when these environment variables are used.
6. Redirect Target URL Encoding/Decoding Issues
What the code actually does:
In src/requests/sessions.py, the get_redirect_target method (lines 134–152) handles the Location header by encoding it to latin1 and then decoding to UTF-8:
location = location.encode("latin1")
return to_native_string(location, "utf8")
Under what conditions it becomes a risk:
This assumes the Location header is ISO-8859-1 encoded (which is the HTTP spec default for headers). However, if the server sends a UTF-8 encoded Location header containing non-ASCII characters, the latin1 encoding will produce mojibake. More critically, if the Location header contains newline characters (CRLF injection), the latin1 encoding will preserve them, potentially enabling HTTP response splitting or header injection attacks if the redirect URL is later used in a context where it's not properly sanitized.
Evidence: src/requests/sessions.py:142-151
Recommended fix: Validate the Location header for CRLF characters before processing. Consider using urllib.parse.urlsplit to validate the URL structure before following the redirect.
7. No Validation of Proxy URL Credentials Format
What the code actually does:
In src/requests/adapters.py, the proxy_headers method (lines 613–632) extracts credentials from the proxy URL using get_auth_from_url and passes them directly to _basic_auth_str:
username, password = get_auth_from_url(proxy)
if username:
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
Under what conditions it becomes a risk:
If a proxy URL contains special characters in the username or password (e.g., colons, @ symbols, or non-ASCII characters), the parsing may produce unexpected results. The _basic_auth_str function (in auth.py:34-75) encodes to latin1, which will raise UnicodeEncodeError for characters outside the latin1 range. This could cause unhandled exceptions or, in some edge cases, credential smuggling.
Evidence: src/requests/adapters.py:626-630, src/requests/auth.py:65-69
Recommended fix: Percent-decode the username and password from the proxy URL before use. Handle UnicodeEncodeError gracefully.
LOW — Informational
8. Weak Hash Used for Digest Authentication (SHA-1 Default)
What the code actually does:
In src/requests/auth.py, the build_digest_header method (lines 157–266) supports MD5, SHA-1, SHA-256, and SHA-512. When no algorithm is specified by the server, it defaults to MD5 (line 170):
if algorithm is None:
_algorithm = "MD5"
All hash functions are called with usedforsecurity=False (e.g., line 179):
return hashlib.md5(x, usedforsecurity=False).hexdigest()
Under what conditions it becomes a risk:
MD5 and SHA-1 are cryptographically broken for collision resistance. While Digest Authentication does not rely on collision resistance (it uses the hash for challenge-response, not for signing), the use of usedforsecurity=False explicitly marks these as non-security-critical. This is technically correct (Digest Auth is not a secure authentication mechanism by modern standards), but it could be misleading to developers who see this and think it provides strong security.
Evidence: src/requests/auth.py:169-205
Recommended fix: Document that Digest Authentication is not cryptographically secure and should not be used for sensitive applications. Consider removing MD5-SESS support.
9. Session State Serialization Includes Sensitive Data
What the code actually does:
In src/requests/sessions.py, the __getstate__ method (line 903) returns a dictionary of attributes listed in __attrs__. The __attrs__ tuple (not shown in the read, but referenced) includes auth, cookies, proxies, headers, etc.
Under what conditions it becomes a risk: If a session is pickled (e.g., for caching or inter-process communication), all authentication credentials (Basic Auth username/password, proxy credentials, cookies) are serialized in plaintext. If the pickle is stored in an insecure location or transmitted over an insecure channel, credentials can be extracted.
Evidence: src/requests/sessions.py:903-905
Recommended fix: Document that session objects contain sensitive data and should be serialized securely. Consider providing a method to strip sensitive fields before serialization.
FALSE POSITIVES
10. verify=False Disables Certificate Validation — By Design, Not a Bug
Claim: The cert_verify method sets cert_reqs = "CERT_NONE" when verify is falsy.
Why it's a false positive: This is intentional and well-documented behavior. The verify parameter is explicitly designed to allow users to disable certificate verification (e.g., for testing with self-signed certificates). The library cannot prevent users from shooting themselves in the foot. The issue is a usability/education problem, not a code defect.
Evidence: src/requests/adapters.py:321-346
11. Basic Auth Credentials Sent in Cleartext Over HTTP — By Design
Claim: _basic_auth_str base64-encodes credentials, which is not encryption.
Why it's a false positive: Base64 encoding is not encryption, but this is the HTTP Basic Authentication standard (RFC 7617). The library correctly implements the standard. The security of Basic Auth depends on TLS, which is the user's responsibility to configure. The library cannot enforce TLS at this level.
Evidence: src/requests/auth.py:34-75
12. No Rate Limiting or Request Throttling — Out of Scope
Claim: The library does not limit the number of redirects or requests.
Why it's a false positive: The library does limit redirects via max_redirects (default 30, line 216 in resolve_redirects). Rate limiting is an application-level concern, not a library feature. The library correctly raises TooManyRedirects when the limit is exceeded.
Evidence: src/requests/sessions.py:216-219
Summary
| # | Severity | Issue | File:Line |
|---|---|---|---|
| 1 | HIGH | SSL verification disabled without warning | adapters.py:321-346 |
| 2 | HIGH | Proxy-Authorization header leakage on HTTPS tunnels | adapters.py:613-632, sessions.py:355-366 |
| 3 | HIGH | Weak cnonce generation and nonce replay in Digest Auth | auth.py:227-237, auth.py:268-271 |
| 4 | MEDIUM | Auth header preserved on http->https redirect (RFC violation) | sessions.py:164-170 |
| 5 | MEDIUM | Environment variable CA bundle override without validation | sessions.py:855-860 |
| 6 | MEDIUM | Redirect Location header encoding issues | sessions.py:142-151 |
| 7 | MEDIUM | No validation of proxy URL credentials format | adapters.py:626-630, auth.py:65-69 |
| 8 | LOW | Weak hash defaults in Digest Auth | auth.py:169-205 |
| 9 | LOW | Session serialization includes plaintext credentials | sessions.py:903-905 |
| 10 | FP | verify=False is intentional design |
adapters.py:321-346 |
| 11 | FP | Base64 Basic Auth is standard behavior | auth.py:34-75 |
| 12 | FP | Redirect limiting exists (max_redirects=30) | sessions.py:216-219 |
Overall Assessment: The Requests library has a well-designed security model for its era, but several issues remain in the Digest Authentication implementation (HIGH), proxy credential handling (HIGH/MEDIUM), and redirect auth stripping logic (MEDIUM). The most critical finding is the weak cnonce generation in Digest Auth, which could enable replay attacks in environments where Digest Auth is used as the primary authentication mechanism.