Back to Blog
critical SEVERITY3 min read

`requests.get()`/`delete()`/`post()` with `verify=False` in Release

A critical security vulnerability in a release automation script disabled SSL certificate verification on every HTTPS request to GitHub's API. By passing `verify=False` to `requests.get()`, `requests.delete()`, and `requests.post()`, the script exposed OAuth tokens and release binaries to man-in-the-middle attacks on any network the script ran from.

O
By Orbis AppSec
Published September 15, 2026Reviewed September 15, 2026

Answer Summary

The `upload-release-asset.py` release script used `verify=False` on four HTTP requests to `api.github.com` and `uploads.github.com`. An attacker on the same network—via ARP spoofing, rogue Wi-Fi, or compromised infrastructure—could intercept these connections, steal the `Authorization` header bearing the OAuth token, and substitute malicious release assets for legitimate ones. The fix removes all four `verify=False` parameters, restoring Python Requests' default certificate validation. No CVE or CWE has been assigned. This is first-party code, not a packaged dependency.

Vulnerability at a Glance

cweunknown
fixRemove `verify=False` to restore default TLS certificate validation
riskMan-in-the-middle attack exposing OAuth tokens and enabling asset substitution
languagePython
root cause`verify=False` passed to `requests.get()`, `requests.delete()`, and `requests.post()`
vulnerabilityImproper Certificate Validation (CWE-295)

Affected Versions

Affected upload-release-asset.py (all versions prior to fix)
Fixed in unknown (first-party code fix)
Ecosystem pypi
CVE / GHSA not assigned
CWE unknown

The Vulnerability Explained

The upload-release-asset.py script automates GitHub release asset management through four HTTPS requests. Each call to the Python requests library included verify=False:

# Vulnerable pattern - repeated four times
r = requests.get(api, headers=h, timeout=60, verify=False)
r = requests.get(assets_url, headers=h, timeout=60, verify=False)
dr = requests.delete("...", headers=h, timeout=60, verify=False)
up = requests.post(url, headers=dict(h, **{"Content-Type": "application/octet-stream"}), 
                   data=f, timeout=900, verify=False)

The verify=False parameter disables TLS certificate validation entirely. When Python Requests encounters this flag, it accepts any certificate presented by the server—even invalid, expired, or attacker-controlled ones—without raising SSLError.

This creates a certificate validation bypass (CWE-295) with two distinct attack surfaces:

  1. Token theft: The headers=h dictionary contains an Authorization: token <oauth_token> header. An attacker intercepting the connection captures this bearer token, gaining the script's full GitHub permissions (typically repo scope for release management).

  2. Asset substitution: The 15-minute upload window (timeout=900) sends binary data to uploads.github.com. An attacker can terminate the TLS connection, serve a malicious file in place of the legitimate release asset, and forward the (now compromised) token to complete the upload—poisoning the release with attacker-controlled code.

The attack requires network-level access: ARP spoofing on a corporate LAN, a compromised VPN concentrator, DNS hijacking, or simply a rogue Wi-Fi access point at a coffee shop where a developer runs the release process.

The Fix

The remediation removes verify=False from all four request sites, restoring Python Requests' default behavior of validating server certificates against system trust stores:

# Fixed: certificate validation now enforced
r = requests.get(api, headers=h, timeout=60)
r = requests.get(assets_url, headers=h, timeout=60)
dr = requests.delete("...", headers=h, timeout=60)
up = requests.post(url, headers=dict(h, **{"Content-Type": "application/octet-stream"}), 
                   data=f, timeout=900)

This single change eliminates the vulnerability because:

  • requests now validates that api.github.com and uploads.github.com present certificates chained to a trusted root CA
  • Certificate mismatches, expiration, or revocation trigger requests.exceptions.SSLError, halting execution before token transmission
  • No code changes are needed elsewhere—the same headers and timeout parameters function identically

Key Takeaways

  • verify=False is never appropriate for production API clients: The parameter exists only for testing against self-signed certificates in controlled environments. Production code using it against public APIs like GitHub's introduces immediate, exploitable MITM risk.

  • Release scripts are supply-chain critical paths: Compromised release assets propagate automatically to users via package managers and update mechanisms. The integrity of these scripts matters as much as the application itself.

  • OAuth tokens in automation require transport-layer protection: Unlike interactive browser flows with PKCE or short-lived codes, bearer tokens in scripts are long-lived and high-privilege. Their exposure grants persistent repository access.

  • Python Requests' defaults are safe; overrides must be justified: The library validates certificates by default. Any override—verify=False, cert_reqs='CERT_NONE' in urllib3, or custom SSLContext—demands explicit risk assessment and documentation.

How Orbis AppSec Detected This

Source: The repo and tag parameters constructing GitHub API URLs (though static in this script, the pattern generalizes to dynamic URL building)

Sink: requests.get(), requests.delete(), and requests.post() invoked with verify=False

Missing control: Certificate validation was explicitly disabled where the default trusted validation should apply; no REQUESTS_CA_BUNDLE override or pinned certificate was present to mitigate

CWE: unknown (vulnerability class: Improper Certificate Validation, CWE-295)

Fix: Remove verify=False from all four HTTP request calls to restore TLS certificate validation against system trust stores

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 verify=False pattern in upload-release-asset.py exemplifies how a single parameter—often added during development to bypass certificate errors—can persist into production with critical consequences. The fix demonstrates that secure defaults, when not overridden, provide adequate protection against network-level attackers. For teams maintaining release automation, audit all HTTP client configurations: any explicit verify=False, ssl=False, or insecure flag warrants immediate review and removal.

Prevention and further reading

Frequently Asked Questions

Why did the script use `verify=False` on GitHub API calls rather than trusting the system CA store?

The original code explicitly disabled verification on all four requests. This was unnecessary—Python Requests validates certificates against system trust stores by default—and created a critical vulnerability where any attacker controlling network path could impersonate GitHub's API.

Which of the four HTTP methods in the release script was most dangerous to leave unverified?

The `POST` to `uploads.github.com` carried the binary release asset, making asset substitution possible; however, the `DELETE` and two `GET` requests were equally severe because their `Authorization` headers contained the OAuth token usable for repository-wide access.

Does removing `verify=False` alone suffice, or does the script need pinned certificates or certificate pinning?

Removing `verify=False` restores standard TLS validation against the system CA store, which is sufficient for GitHub's publicly trusted certificates. Pinning would add fragility without meaningful security gain against this threat model.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #19

Related Articles

critical

ExternalHttpClient::request() Sent Basic Auth Over Plain HTTP

The `ExternalHttpClient::request()` helper accepted a `$basicAuth` string and passed it straight to the HTTP client's `auth` option without checking that the target URL used `https://`. Any external JSON data source configured with an `http://` endpoint therefore shipped a base64-encoded `Authorization: Basic` header in cleartext on every scheduled load. The fix rejects the request outright — before a client is even created — when the URL scheme is not HTTPS.

critical

How insufficient PBKDF2 iterations happen in JavaScript and how to fix it

A critical vulnerability in `libs/wgs/pbkdf2.js` used only 1 iteration for PBKDF2 password hashing, making passwords trivially crackable. The fix increases iterations to 600,000, aligning with OWASP 2023 recommendations and preventing GPU-accelerated brute-force attacks.

critical

How Hardcoded Encryption Salts Compromise Credential Storage in Node.js and How to Fix It

A critical vulnerability in `scripts/bench-cpu.js` used a hardcoded static salt (`'byok-relay-salt'`) when deriving encryption keys with scrypt, allowing attackers to decrypt all encrypted credentials if the encryption secret was compromised. The fix replaces the hardcoded salt with cryptographically secure random bytes generated per operation, ensuring each user's encrypted credentials require a unique derived key.

high

How weak scrypt password hashing happens in Node.js and how to fix it

The `hashPass` function in `store-saas/server.mjs` used Node.js's `crypto.scryptSync` with default cost parameters (N=16384, r=8, p=1), making stored password hashes cheap to attack with modern GPUs. The fix increases the CPU/memory cost factor to N=131072 and parallelization to p=2, dramatically raising the computational effort required to brute-force stolen hashes.

high

How Dependency Version Pinning Prevents Supply Chain Attacks in Node.js and How to Fix It

A critical supply chain vulnerability in `package.json` allowed automatic updates to a cryptographic library with known weaknesses. By pinning `rijndael-js` to version `2.0.0` instead of allowing `^2.0.0` updates, the fix prevents silent installation of vulnerable versions that could expose downstream consumers to weak block cipher modes and authentication bypasses.

critical

LDAP Filter Injection in da_unique_email_validator Fixed

The registration-time email uniqueness validator, `da_unique_email_validator`, formatted the submitted email address straight into an LDAP search filter with Python's `%` operator, so filter metacharacters in the email were interpreted as filter syntax. The fix wraps the value in `ldap.filter.escape_filter_chars()` (and imports the `ldap.filter` submodule explicitly), so a submitted address is always treated as a literal attribute value. Any deployment with `ldap login` enabled and a bind accoun