Python's standard library ships ssl, socket, and http.client, but almost no production service uses them directly for anything beyond basic connectivity. Most Python codebases lean on requests, urllib3, cryptography, PyOpenSSL, paramiko, or PyNaCl to handle TLS negotiation, certificate validation, and encrypted transport — and each of those libraries has shipped security-relevant CVEs in the last three years. urllib3 alone has had two header-leak vulnerabilities patched in 2023 and 2024 (CVE-2023-43804, CVE-2024-37891), and the Terrapin SSH downgrade attack disclosed in December 2023 (CVE-2023-48795) affected paramiko alongside OpenSSH. This post walks through which Python networking libraries actually enforce secure defaults, where they've failed in practice, and what to check in your own dependency tree before assuming "we use requests, so we're fine" is a real security posture.
Which Python libraries actually handle secure network communication?
Six libraries cover the overwhelming majority of secure networking in Python code: ssl (stdlib TLS), requests and urllib3 (HTTP/HTTPS clients), cryptography and PyOpenSSL (crypto primitives and TLS context building), and paramiko (SSH). requests pulls in urllib3 as its transport layer and certifi for its CA bundle, so a vulnerability in either propagates directly into requests-based code without any additional import. cryptography (maintained by the Python Cryptographic Authority) backs PyOpenSSL, paramiko, and increasingly pyca-adjacent projects, and as of mid-2026 it pulls over 700 million downloads a month from PyPI, making it one of the most widely deployed crypto implementations in any language ecosystem. PyNaCl, a binding to libsodium, is the go-to when a project wants modern, misuse-resistant primitives (Ed25519, XSalsa20-Poly1305) instead of raw OpenSSL bindings. Knowing this map matters because a vulnerability disclosure for urllib3 doesn't just affect projects that import urllib3 — it affects every requests call in your stack, transitively.
Why does requests fail to catch certificate errors that developers assume it catches?
requests fails silently on certificate validation the moment a developer passes verify=False or points REQUESTS_CA_BUNDLE at a permissive corporate proxy bundle, and both patterns show up constantly in real codebases. A 2024 audit of public GitHub repositories found verify=False hardcoded in requests.get/requests.post calls across thousands of open-source projects, frequently introduced during local debugging and never removed before merge. Once disabled, requests will happily complete a TLS handshake with an expired, self-signed, or actively MITM'd certificate and return a 200 response with no warning in application logs — urllib3 does emit an InsecureRequestWarning, but Python's default warning filters suppress it after the first occurrence per location, so it's easy to miss in noisy CI output. The fix isn't a code review policy alone; it's static and runtime detection of verify=False, custom ssl.create_default_context() calls with check_hostname=False, or ssl.CERT_NONE anywhere in the call graph that reaches a production network client.
What CVEs have actually hit Python's secure networking stack?
Four disclosures in the last 30 months illustrate that this is not a theoretical risk category. CVE-2023-32681, patched in requests 2.31.0 (May 2023), leaked Proxy-Authorization headers to destination servers when a redirect crossed origins through an HTTP proxy — meaning proxy credentials could be exfiltrated to any server an attacker could redirect a request toward. CVE-2023-43804, patched in urllib3 2.0.7 (October 2023), leaked Cookie headers in the same cross-origin-redirect pattern, and CVE-2024-37891 (June 2024) found the identical Proxy-Authorization leak still reachable through a different code path in urllib3's redirect handling, requiring a second patch a year later. Most consequentially, CVE-2023-48795 — the Terrapin attack disclosed in December 2023 — exploited a weakness in the SSH transport protocol's sequence-number handling during extension negotiation, allowing an on-path attacker to strip security-relevant messages from the handshake; paramiko shipped a fix in 3.4.0 alongside a coordinated patch across OpenSSH, libssh, and other SSH implementations. None of these required exotic exploitation — they required an application making outbound requests through a proxy, following redirects, or connecting to attacker-influenced SSH endpoints, which describes a large share of production Python services.
How do urllib3, PyOpenSSL, and paramiko compare on default security posture?
urllib3 and PyOpenSSL default to strict certificate verification, while paramiko requires the developer to explicitly choose a host-key policy, and picking the wrong one silently disables SSH's core security guarantee. urllib3's PoolManager verifies certificates against certifi's CA bundle by default since version 1.25 (2019) and will raise SSLError on mismatch. PyOpenSSL, used as an alternate SSL backend for urllib3 in older requests installations, inherits the same default-verify behavior when wrapped through pyopenssl.PyOpenSSLContext. paramiko, however, ships AutoAddPolicy as an easy option that developers frequently paste from Stack Overflow: it accepts and stores any unknown host key without prompting, which defeats host-key pinning entirely and makes SSH connections trivially vulnerable to on-path attackers on first connect. The secure equivalent, RejectPolicy combined with a pre-populated known_hosts file, requires more setup and shows up in a small minority of the paramiko-using repositories we've reviewed. This is a case where the "convenient" default in widely copied example code is the insecure one.
What should a dependency-pinning strategy for these libraries actually enforce?
A pinning strategy for Python networking libraries needs to enforce minimum patched versions, not just "latest compatible," because semantic-versioning-compatible ranges (requests>=2.28,<3) can still resolve to a pre-patch release. Concretely: requests>=2.31.0 closes CVE-2023-32681, urllib3>=2.0.7 closes CVE-2023-43804, urllib3>=2.2.2 closes CVE-2024-37891, and paramiko>=3.4.0 closes the Terrapin downgrade. pip's resolver will happily install requests==2.28.1 today if that's what a lockfile specifies, with no runtime warning that it's three security patches behind. Teams running pip-audit or safety check in CI catch known-CVE versions at build time, but both tools rely on the same PyPI/OSV advisory feed, so a zero-day window exists between disclosure and database update — typically 24-72 hours based on OSV's historical publication lag. The more durable control is generating and diffing a Software Bill of Materials on every build so a security team can query "which services still resolve to urllib3 under 2.0.7" in seconds rather than grepping lockfiles across dozens of repos.
How Safeguard Helps
Safeguard's reachability analysis determines whether a vulnerable code path in urllib3's redirect handling, paramiko's host-key policy, or requests' proxy-header logic is actually exercised by your application's call graph, so security teams triage CVE-2024-37891 or CVE-2023-48795 by exploitability instead of chasing every match in a dependency tree. Griffin, Safeguard's AI analysis engine, reads the surrounding code to flag risky patterns directly — a hardcoded verify=False, a paramiko.AutoAddPolicy() instantiation, or a TLS context with check_hostname=False — even when the library version itself isn't flagged by any CVE database. Continuous SBOM generation and ingest gives teams a live, queryable inventory of exactly which services depend on which version of requests, urllib3, cryptography, or paramiko, closing the gap between advisory publication and internal visibility. When a fix is available, Safeguard opens an auto-fix pull request that bumps the pinned version and reruns your test suite, turning a multi-repo remediation effort into a reviewable diff.