Back to Blog
medium SEVERITY6 min read

How gitlab.bandit.B501 happens in Python and how to fix it

The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.

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

Answer Summary

This is gitlab.bandit.B501, a Python vulnerability where the `requests` library is called with `verify=False`, disabling TLS certificate validation (CWE-295: Improper Certificate Validation). It was found in `get_soup()` in `proverbia-scraper.py`. The fix removes the `verify=False` argument (letting `requests` default to `verify=True`) and deletes the `urllib3.disable_warnings()` call that had been masking the insecure-request warning.

Vulnerability at a Glance

cweCWE-295 (Improper Certificate Validation)
fixRemoved `verify=False` and the warning suppression so `requests` validates certificates by default
riskMan-in-the-middle attackers can intercept or tamper with HTTPS traffic undetected
languagePython
root cause`requests.get()` called with `verify=False`, and warnings suppressed via `urllib3.disable_warnings()`
vulnerabilityDisabled TLS Certificate Verification (Bandit B501)

Introduction

The proverbia-scraper.py file handles a simple but common task: fetching pages from https://proverbia.net/ and parsing them with BeautifulSoup to pull out proverbs. The heavy lifting happens in a single helper function, get_soup(url), which wraps every outbound HTTP call the scraper makes. A flaw in that one function, however, meant that every single request the scraper issued was vulnerable to interception.

The problem lived on what used to be line 33:

page = requests.get(url, headers=HEADERS, timeout=30, verify=False)

That verify=False argument tells the requests library to skip TLS certificate validation entirely. Combined with a urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) call earlier in the file, the code wasn't just insecure — it was insecure silently, with no warning ever surfacing in logs or console output.

The Vulnerability Explained

Here's the vulnerable code as it existed before the fix:

import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

BASE_URL = "https://proverbia.net/"
NOW = datetime.now()

...

def get_soup(url: str) -> BeautifulSoup:
    """Download a web page and return it as a BeautifulSoup object."""
    page = requests.get(url, headers=HEADERS, timeout=30, verify=False)
    page.raise_for_status()
    return BeautifulSoup(page.content, "html.parser")

Two things are happening here, and both matter:

  1. verify=False disables TLS certificate validation. Normally, when requests connects to https://proverbia.net/, it checks the server's certificate against a trusted certificate authority chain and verifies the hostname matches. With verify=False, none of that happens — requests will happily complete the TLS handshake with any server presenting any certificate, including a self-signed one from an attacker.

  2. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) suppresses the InsecureRequestWarning that urllib3 would normally print to stderr every time an unverified HTTPS request is made. This is the giveaway that someone added verify=False deliberately and then silenced the tool trying to warn them about it — a pattern that removes the last line of defense (a visible warning in logs) against the misconfiguration going unnoticed.

Attack scenario: Imagine this scraper running on a schedule inside a CI job, a cron container, or a shared network (a coffee shop Wi-Fi, a compromised corporate proxy, a poisoned DNS cache). An attacker positioned on the network path between the scraper and proverbia.net can perform a classic man-in-the-middle attack:

  • The attacker intercepts the DNS resolution or ARP-spoofs the local network so traffic to proverbia.net routes through their machine.
  • They present their own TLS certificate — self-signed, expired, or issued for a completely different domain.
  • Because get_soup() calls requests.get(url, ..., verify=False), requests accepts this bogus certificate without complaint.
  • The attacker now sits inside the encrypted tunnel, able to read every byte of the response (page.content) before it's handed to BeautifulSoup, and — more dangerously — can rewrite the HTML the scraper parses.

Since get_soup()'s return value drives whatever logic consumes the parsed proverbs (storage, display, further processing), an attacker who controls the response content controls what the scraper "sees." Depending on how downstream code trusts the parsed data, this could range from serving fake/misleading content to injecting malicious payloads if the scraped text is ever rendered, executed, or stored without further sanitization.

The Fix

The fix is minimal but precise — it removes the insecure argument and the warning suppression that was hiding it:

Before:

import requests
from random import choice
from datetime import datetime
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

BASE_URL = "https://proverbia.net/"
NOW = datetime.now()

def get_soup(url: str) -> BeautifulSoup:
    """Download a web page and return it as a BeautifulSoup object."""
    page = requests.get(url, headers=HEADERS, timeout=30, verify=False)
    page.raise_for_status()
    return BeautifulSoup(page.content, "html.parser")

After:

import requests
from random import choice
from datetime import datetime
BASE_URL = "https://proverbia.net/"
NOW = datetime.now()

def get_soup(url: str) -> BeautifulSoup:
    """Download a web page and return it as a BeautifulSoup object."""
    page = requests.get(url, headers=HEADERS, timeout=30)
    page.raise_for_status()
    return BeautifulSoup(page.content, "html.parser")

Two coordinated changes made this work:

  1. verify=False was dropped entirely from the requests.get() call in get_soup(). Since requests defaults to verify=True, simply omitting the argument restores full certificate chain and hostname validation. requests will now reject any connection to proverbia.net that doesn't present a valid, trusted certificate for that hostname.

  2. The urllib3 import and disable_warnings() call were deleted. Once certificate verification is back on, there's no InsecureRequestWarning to suppress — but removing this code also eliminates the risk that a future contributor copies the verify=False pattern elsewhere in the file without noticing the warning had been silenced project-wide.

Notably, the fix preserved timeout=30, which was already correctly set — this wasn't a timeout issue, it was purely a certificate-validation issue. The PR is scoped to exactly one file and one function, so no behavioral change occurs for legitimate traffic: valid HTTPS connections to proverbia.net continue to work exactly as before, while spoofed or attacker-controlled certificates are now correctly rejected.

Prevention & Best Practices

  • Never set verify=False in production code. If you're debugging a certificate issue locally, use environment-specific configuration (e.g., a custom CA bundle path passed to verify) rather than disabling validation outright.
  • Always pair TLS calls with an explicit timeout, as this code already did with timeout=30 — a missing timeout can hang indefinitely and is a separate but related hardening concern in the same Bandit rule (B501/B113 family).
  • Treat suppressed warnings as a red flag during code review. A call to urllib3.disable_warnings(...) or warnings.filterwarnings("ignore") near HTTP code should always prompt a second look — it often exists specifically to hide a security misconfiguration.
  • Run static analysis in CI. Tools like Bandit and Semgrep can catch verify=False and similar patterns automatically, long before the code reaches production.
  • Reference OWASP guidance on transport security: the OWASP Transport Layer Protection Cheat Sheet covers why certificate validation is non-negotiable for any client making outbound HTTPS calls.

Key Takeaways

  • The get_soup() function in proverbia-scraper.py was making every HTTPS request to proverbia.net without validating the server's certificate — a single verify=False argument undermined TLS for the entire scraper.
  • The accompanying urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) call was actively hiding the security warning that would have flagged this misconfiguration.
  • The fix required no new logic — deleting verify=False and the warning suppression was sufficient, since requests defaults to secure behavior (verify=True).
  • The existing timeout=30 was correctly preserved, showing that not every part of a flagged pattern needs to change — only the actual insecure argument did.
  • Any future requests calls added to this scraper should be checked for the same pattern before merging.

How Orbis AppSec Detected This

  • Source: The url parameter passed into get_soup(url), ultimately derived from BASE_URL and scraper-constructed page URLs targeting https://proverbia.net/.
  • Sink: requests.get(url, headers=HEADERS, timeout=30, verify=False) in proverbia-scraper.py:33.
  • Missing control: TLS certificate validation was explicitly disabled via verify=False, and the resulting InsecureRequestWarning was suppressed via urllib3.disable_warnings(), removing both the protection and the visible warning of its absence.
  • CWE: CWE-295 — Improper Certificate Validation.
  • Fix: Removed verify=False from the requests.get() call and deleted the urllib3.disable_warnings() call, restoring default certificate validation for all outbound requests.

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

A single argument — verify=False — was enough to strip TLS protection from every outbound request in proverbia-scraper.py, and a companion call to urllib3.disable_warnings() made sure no one would notice. The fix demonstrates that hardening doesn't always require complex logic changes: sometimes the most secure code is the code you remove. Going forward, treat verify=False and warning suppression calls as immediate red flags in code review, and let static analysis tools like Bandit and Semgrep catch these patterns automatically before they ship.

References

  • CWE-295: Improper Certificate Validation — https://cwe.mitre.org/data/definitions/295.html
  • OWASP Transport Layer Security Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html
  • Python requests documentation, SSL Cert Verification — https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification
  • Semgrep rule for insecure requests usage — https://semgrep.dev/r?q=python.requests.security.disabled-cert-validation
  • harden: the application was found using the requests ... in...

Frequently Asked Questions

What is gitlab.bandit.B501?

It's a Bandit/Semgrep rule that flags Python `requests` calls made with `verify=False`, which disables TLS certificate validation and exposes the connection to man-in-the-middle attacks.

How do you prevent gitlab.bandit.B501 in Python?

Never pass `verify=False` to `requests` calls; let it default to `verify=True` (or explicitly set it), and always pair it with an explicit `timeout` value to avoid hanging connections.

What CWE is gitlab.bandit.B501?

It maps to CWE-295, Improper Certificate Validation.

Is suppressing the InsecureRequestWarning with urllib3.disable_warnings() enough to prevent this vulnerability?

No — suppressing the warning only hides the symptom; it does not restore certificate validation and leaves the connection vulnerable to MITM attacks.

Can static analysis detect gitlab.bandit.B501?

Yes, tools like Bandit and Semgrep can statically flag `verify=False` usage in `requests` calls, which is exactly how this issue was caught before it reached production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #877

Related Articles

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

A critical Denial of Service vulnerability (CVE-2026-59873) in the `tar` npm package allowed attackers to craft malicious gzip archives that could exhaust memory or CPU during decompression. The fix upgrades `tar` from 7.5.11 to 7.5.21 across `package.json` and `package-lock.json`, closing the resource-exhaustion path without changing any application code.