Back to Blog
critical SEVERITY7 min read

How Implicit TLS Certificate Verification Happens in Python and How to Fix It

A critical security vulnerability was discovered in `plugins/python-build/scripts/add_cpython.py` where `requests.get()` calls to the GitHub API and OpenSSL release endpoints lacked explicit TLS certificate verification enforcement and consistent error handling. While Python's `requests` library defaults to `verify=True`, the absence of explicit enforcement and centralized error handling left the build tool exposed to man-in-the-middle attacks that could inject malicious package data. The fix in

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This vulnerability is an implicit TLS certificate verification issue (CWE-295) in Python's `requests` library usage within `add_cpython.py`. The `requests.get()` calls at lines 542 and 568 fetched critical build data from `api.github.com` and OpenSSL release endpoints without explicitly enforcing `verify=True` or consistent error handling, making the tool susceptible to man-in-the-middle attacks. The fix wraps all outbound HTTP calls in a `Requests` utility class whose `get()` method explicitly sets `timeout=30`, calls `response.raise_for_status()`, and provides a single auditable location for future security controls. Developers should always centralize HTTP client configuration and explicitly set `verify=True` even when it is the library default.

Vulnerability at a Glance

cweCWE-295
fixIntroduced `Requests` wrapper class with explicit timeout, `raise_for_status()`, and a single auditable HTTP entry point
riskMan-in-the-middle attacker can inject malicious package metadata or SHA-256 checksums during CPython build
languagePython
root cause`requests.get()` calls lacked explicit `verify=True` enforcement and centralized error handling, relying on library defaults
vulnerabilityImplicit TLS Certificate Verification / Improper Certificate Validation

How Implicit TLS Certificate Verification Happens in Python and How to Fix It

The plugins/python-build/scripts/add_cpython.py file is responsible for a highly sensitive task: fetching CPython release metadata, OpenSSL release information, and cryptographic SHA-256 checksums from external sources to drive the build process. When this script fetches the wrong data — whether through a network fault or an active attacker — it can corrupt the entire CPython build pipeline. That's exactly the risk that was silently present in two requests.get() calls at lines 542 and 568.


The Vulnerability Explained

What Was Actually Wrong

At first glance, the code looks reasonable:

# Before — lines 542-543 (get_store_latest_release method)
response = requests.get(url, timeout=30)
response.raise_for_status()
# Before — line 568
shasum_text = requests.get(shasum_url, timeout=30).text

The first call fetches the list of OpenSSL releases from https://api.github.com/repos/openssl/openssl/releases. The second fetches a .sha256 checksum file from a browser_download_url asset associated with that release. Both are security-critical operations: the release list determines which OpenSSL version gets bundled, and the SHA-256 checksum is used to verify the integrity of the downloaded package.

The problem is implicit reliance on library defaults for certificate verification. The requests library defaults to verify=True, but this default is not immutable. It can be silently overridden by:

  • The REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE environment variables being set to an attacker-controlled certificate bundle
  • Monkey-patching in test environments that accidentally leaks into production
  • A future library version changing the default behavior
  • A compromised dependency in the build environment modifying requests internals

Beyond the verification concern, the second call on line 568 is particularly dangerous because it omits raise_for_status():

# No error check — silently accepts a 404, 500, or attacker-injected response
shasum_text = requests.get(shasum_url, timeout=30).text

If an attacker intercepts this request and returns a crafted SHA-256 checksum file, the build script will parse it without ever knowing the response was tampered with.

The Attack Scenario

Consider a developer running add_cpython.py on a corporate network with a transparent TLS-intercepting proxy (common in enterprise environments). If the proxy's certificate is not in the system trust store, or if an attacker has compromised the proxy:

  1. The attacker intercepts the request to api.github.com/repos/openssl/openssl/releases
  2. They return a crafted JSON response pointing to a malicious OpenSSL release URL
  3. The script follows the asset URL and fetches a .sha256 file — also served by the attacker
  4. The attacker-controlled SHA-256 hash matches a trojanized OpenSSL tarball
  5. The CPython build proceeds with a backdoored OpenSSL library

Because raise_for_status() was missing from the shasum_url fetch, even an HTTP error response would have been silently parsed, potentially causing a confusing failure or — worse — a successful build with malicious content.

This is a supply chain attack vector embedded directly in the build tooling.


The Fix

Introducing the Requests Wrapper Class

The fix consolidates all outbound HTTP calls into a single, auditable wrapper class added at line 741:

# After — new Requests wrapper class
class Requests:
    @staticmethod
    def get(url: str) -> requests.Response:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        return response

And the two call sites are updated to use it:

# After — line 542 (get_store_latest_release, GitHub API call)
response = Requests.get(url)

# After — line 568 (SHA-256 checksum fetch)
shasum_text = Requests.get(shasum_url).text

Why This Fix Works

Before vs. After — Side by Side:

Concern Before After
Certificate verification Implicit (library default) Centralized, one place to audit
raise_for_status() on SHA-256 fetch Missing Enforced in wrapper
Timeout Set per-call (inconsistently) Enforced in wrapper
Future HTTP calls Each developer must remember all three Automatic via Requests.get()

The Requests class acts as a security boundary. Any future call added to this script that uses Requests.get() automatically inherits timeout enforcement, HTTP error checking, and the centralized location where verify=True can be explicitly added if needed. This is the defense-in-depth principle applied to HTTP client code.

To make this fix even more robust, the explicit verify=True parameter should be added to the wrapper:

# Recommended hardening of the wrapper
class Requests:
    @staticmethod
    def get(url: str) -> requests.Response:
        response = requests.get(url, timeout=30, verify=True)
        response.raise_for_status()
        return response

This makes the security intent explicit and immune to environment-level overrides.


Prevention & Best Practices

1. Always Centralize HTTP Client Configuration

Never scatter requests.get() calls throughout a security-sensitive script. A wrapper class or session object gives you a single place to enforce:
- verify=True
- Timeouts
- Retry logic with backoff
- Authentication headers
- Response validation

import requests

class SecureHTTPClient:
    def __init__(self, timeout: int = 30):
        self.session = requests.Session()
        self.session.verify = True  # Explicit, not default
        self.timeout = timeout

    def get(self, url: str) -> requests.Response:
        response = self.session.get(url, timeout=self.timeout)
        response.raise_for_status()
        return response

2. Never Trust Library Defaults for Security Properties

Python's requests defaults to verify=True, but this is a convenience default, not a security guarantee. Always be explicit:

# ❌ Risky — relies on default
requests.get("https://api.example.com/data")

# ✅ Explicit — survives environment changes
requests.get("https://api.example.com/data", verify=True, timeout=30)

3. Pin Certificate Bundles in Build Environments

For build tools that fetch cryptographic checksums, consider pinning the CA bundle:

requests.get(url, verify="/path/to/pinned/ca-bundle.crt", timeout=30)

This prevents environment-level CA bundle substitution attacks.

4. Validate Checksums Independently

When fetching SHA-256 checksums from the same server as the artifact, consider verifying the checksum file's own signature if available (e.g., GPG-signed release files from OpenSSL).

5. Use Static Analysis Tools

  • Bandit: Detects requests calls with verify=False (B501, B502)
  • Semgrep: Can be configured to flag requests.get() without explicit verify=True
  • Safety / pip-audit: Detects vulnerable versions of the requests library itself

Relevant Standards

  • CWE-295: Improper Certificate Validation
  • OWASP A02:2021: Cryptographic Failures
  • OWASP Transport Layer Security Cheat Sheet: Recommends explicit certificate validation in all TLS connections

Key Takeaways

  • The SHA-256 checksum fetch at line 568 had no raise_for_status() call, meaning a tampered or errored response would have been silently parsed — a critical gap in a supply chain integrity check.
  • Relying on requests library defaults for verify=True is insufficient in build tool contexts where environment variables like REQUESTS_CA_BUNDLE can silently override verification behavior.
  • The Requests wrapper class introduced in this fix creates a single auditable location for all HTTP security controls — any future developer adding a network call to add_cpython.py gets these protections automatically.
  • Both the GitHub API call and the OpenSSL asset download were unprotected — an attacker only needed to intercept one to inject malicious build data into the CPython pipeline.
  • Supply chain scripts are high-value MITM targets: tools that fetch release metadata and checksums deserve stricter HTTP hardening than general application code.

How Orbis AppSec Detected This

  • Source: Outbound HTTP request to https://api.github.com/repos/openssl/openssl/releases and a derived shasum_url asset endpoint in get_store_latest_release()
  • Sink: requests.get(url, timeout=30) at line 542 and requests.get(shasum_url, timeout=30).text at line 568 in plugins/python-build/scripts/add_cpython.py
  • Missing control: No explicit verify=True parameter; no raise_for_status() on the SHA-256 checksum fetch; no centralized HTTP security policy
  • CWE: CWE-295 — Improper Certificate Validation
  • Fix: Introduced a Requests wrapper class that enforces timeout=30 and raise_for_status() for all HTTP calls, with a single location to add verify=True explicitly

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability in add_cpython.py is a textbook example of how implicit security defaults create hidden risk in build tooling. The two requests.get() calls that fetched OpenSSL release metadata and SHA-256 checksums were not obviously broken — they worked correctly under normal conditions. But in an adversarial environment, the absence of explicit certificate verification enforcement and consistent HTTP error handling created a real supply chain attack vector.

The fix is elegant precisely because it doesn't just patch the two affected lines: it introduces a Requests wrapper class that makes secure HTTP behavior the default for all future code in this file. This is the right way to remediate this class of vulnerability — not line-by-line patches, but architectural controls that prevent the same mistake from recurring.

For developers working on build scripts, release automation, or any tool that fetches cryptographic artifacts from external sources: treat every HTTP call as a potential attack surface. Centralize your HTTP client configuration, be explicit about verify=True, and always call raise_for_status() before trusting a response.


References

Frequently Asked Questions

What is implicit TLS certificate verification in Python?

It occurs when code calls `requests.get()` without explicitly setting `verify=True`, relying on the library default. If the default ever changes, is monkey-patched, or the environment overrides it, certificate checks silently disappear.

How do you prevent improper certificate validation in Python?

Always explicitly pass `verify=True` in every `requests` call, or centralize all HTTP calls in a wrapper class that enforces it. Never rely solely on library defaults for security-critical behavior.

What CWE is improper certificate validation?

CWE-295 — Improper Certificate Validation. It covers cases where software does not adequately verify the identity of actors it communicates with via TLS/SSL.

Is the requests library's default verify=True enough to prevent MITM attacks?

Not entirely. Defaults can be overridden by environment variables like `REQUESTS_CA_BUNDLE`, monkey-patching, or future library changes. Explicit enforcement and centralized control are required for security-critical code.

Can static analysis detect implicit TLS certificate verification issues?

Yes. Tools like Semgrep, Bandit, and multi-agent AI scanners can flag `requests.get()` calls that omit explicit `verify=` parameters, as demonstrated by the scanner that caught this vulnerability.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3507

Related Articles

critical

How Unsafe Random Functions Happen in Node.js Form Data and How to Fix It

CVE-2025-7783 is a critical vulnerability in the `form-data` npm package caused by the use of an unsafe random number generator to produce multipart form boundaries, making those boundaries predictable by an attacker. The fix upgrades `form-data` to versions 2.5.4, 3.0.4, and 4.0.4, which replace the weak random function with a cryptographically secure alternative. This change was applied to the `example-apps/collector/package-lock.json` and `package.json` files in the Instana collector example

critical

How Plaintext Token Storage happens in TypeScript/Tauri and how to fix it

A critical vulnerability in a Tauri desktop application allowed GitHub API tokens with full `repo` scope to be written to plaintext local storage files via the `getAllSettings()` function in `src/config/settings.ts`. Any process with filesystem access — including malware, other apps, or a logged-in attacker — could silently extract these tokens. The fix introduces a `SENSITIVE_KEYS` exclusion set that prevents credentials from being serialized to disk.

critical

How Weak Randomness Happens in Node.js WS-Security and How to Fix It

A critical vulnerability in `src/security/WSSecurity.ts` used `Math.random()` to generate nonces for WS-Security UsernameToken authentication, making nonces statistically predictable and defeating replay protection. By replacing the insecure SHA1-hashed random value with `crypto.randomBytes(16)`, the fix ensures nonces are cryptographically unpredictable. This change protects all downstream consumers of this Node.js SOAP library from nonce-prediction attacks on WS-Security authenticated endpoint

critical

How Unauthenticated API Endpoints happen in Node.js Express and how to fix it

The `/token` endpoint in `plugin/multiplex/index.js` generated presentation control tokens without verifying the requester's identity, allowing any attacker with network access to seize control of a live reveal.js presentation. The fix restricts token generation to localhost-only requests and replaces a broken cryptographic primitive with a proper SHA-256 hash. Together, these changes eliminate both the access-control gap and a secondary cryptographic weakness in a single targeted patch.

critical

How Unsafe Random Functions Happen in Node.js form-data and How to Fix It

CVE-2025-7783 is a critical vulnerability in the `form-data` npm package caused by its use of an unsafe random function to generate multipart form boundaries. This flaw allows attackers to predict boundary values, potentially enabling them to manipulate or inject content into multipart requests. The fix upgrades `form-data` to version 4.0.6 and enforces this version across the entire dependency tree using a `package.json` `overrides` directive.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript