CVE-2026-44502
URL parsing mismatch in Bugsink's webhooks leads to an SSRF vulnerability.
- CVSS 4.3
- CWE-918
- Input Validation and Sanitization
- Remote
Bugsink is a self-hosted error tracking tool. Prior to 2.1.3, Bugsink’s webhook URL validation could be (partially) bypassed because of a mismatch in URL parsing. The original validation logic parsed webhook URLs with Python’s urllib.parse.urlparse, then sent the request with requests.post. For malformed inputs involving backslashes and @, those components can disagree about where the authority ends and which hostname is the real target. A URL may therefore appear to target an allowlisted public hostname during validation, while the HTTP client actually connects to a different host. This vulnerability is fixed in 2.1.3.
- CWE
- CWE-918
- CVSS base score
- 4.3
- Published
- 2026-05-26
- OWASP
- A10 Server-Side Request Forgery (SSRF)
- Orthogonal defect classification
- Interface
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Server-Side Request Forgery (SSRF)
- Accessibility scope
- Remote
- Impact
- Unauthorized Access
- Affected component
- Bugsink
- Fixed by upgrading
- Yes
Solution
Upgrade Bugsink to version 2.1.3 or later.
Vulnerable code sample
import requests
from urllib.parse import urlparse
# A simplified representation of the Bugsink webhook logic
# vulnerable prior to version 2.1.3.
ALLOWED_WEBHOOK_HOSTS = ['bugsink.io', 'example.com']
def trigger_webhook(url: str, data: dict):
"""
Validates a webhook URL and sends a request. This version is vulnerable.
"""
try:
# 1. Validation logic uses `urllib.parse.urlparse`.
# For a malicious URL like "http://example.com\@@127.0.0.1/p",
# `parsed_url.hostname` correctly identifies 'example.com'.
parsed_url = urlparse(url)
hostname = parsed_url.hostname
if hostname not in ALLOWED_WEBHOOK_HOSTS:
print(f"Error: Host '{hostname}' is not in the allowlist.")
return
# 2. Request logic uses the `requests` library.
# Due to a parsing mismatch, `requests` interprets the same URL
# "http://example.com\@@127.0.0.1/p" as targeting '127.0.0.1',
# thus bypassing the host validation.
print(f"Validation passed. Sending request to {url}...")
requests.post(url, json=data, timeout=2)
except Exception as e:
print(f"Request failed: {e}")Patched code sample
import socket
import ipaddress
import requests
from urllib.parse import urlparse
# An allowlist of trusted hostnames for webhooks
ALLOWED_HOSTS = {'good.example.com', 'api.service.com'}
def send_validated_webhook(url: str, data: dict):
"""
Parses, validates, and sends a webhook request, preventing SSRF.
This fixed version introduces two key security controls:
1. It rejects URLs containing characters known to cause parser confusion.
2. It resolves the hostname to an IP address and validates that the IP
is not a private, reserved, or loopback address. This ensures the
request's true destination is a public server.
"""
try:
parsed_url = urlparse(url)
# 1. Basic validation: ensure scheme and hostname are present.
if parsed_url.scheme not in ('http', 'https'):
raise ValueError("Invalid URL scheme. Only http and https are allowed.")
hostname = parsed_url.hostname
if not hostname:
raise ValueError("URL must contain a hostname.")
# 2. Reject URLs with characters that can cause parser differential issues.
# The original vulnerability was caused by backslashes in the netloc.
if '\\' in parsed_url.netloc:
raise ValueError("Invalid characters in URL authority section.")
# 3. Check hostname against a strict allowlist.
if hostname not in ALLOWED_HOSTS:
raise ValueError(f"Hostname '{hostname}' is not in the allowed list.")
# 4. Resolve the hostname to an IP address to find the true destination.
# This is the most critical step to mitigate SSRF.
resolved_ip_str = socket.gethostbyname(hostname)
resolved_ip = ipaddress.ip_address(resolved_ip_str)
# 5. Prevent requests to internal/private network addresses.
if resolved_ip.is_private or resolved_ip.is_loopback or resolved_ip.is_reserved:
raise ValueError(f"Resolved IP address '{resolved_ip}' is not a public address.")
# 6. If all checks pass, the URL is considered safe to request.
print(f"Validation passed. Sending request to {url} (resolved to {resolved_ip}).")
response = requests.post(url, json=data, timeout=5)
response.raise_for_status()
print("Request sent successfully.")
return response
except (ValueError, socket.gaierror, requests.exceptions.RequestException) as e:
print(f"Webhook failed validation or execution: {e}")
return None
# Example usage demonstrating the fix
# A legitimate URL that will pass validation
# good.example.com must resolve to a public IP for this to work.
# To test, add "1.2.3.4 good.example.com" to your /etc/hosts file.
# send_validated_webhook("https://good.example.com/webhook", data={"status": "ok"})
# A malicious URL attempting to bypass validation.
# urlparse sees 'good.example.com', but requests could be tricked.
# This version catches it with the backslash check.
# send_validated_webhook("https://good.example.com\\@localhost:8000/api", data={"status": "pwned"})
# Another SSRF attempt using a valid but internal-resolving name.
# This is blocked by the IP address check.
# (Assumes 'intranet.service' resolves to a private IP like 192.168.1.10)
# To test, add "192.168.1.10 intranet.service" to your /etc/hosts and
# 'intranet.service' to ALLOWED_HOSTS.
# send_validated_webhook("http://intranet.service/status", data={"user": "admin"})Payload
https://allowlisted.com\@127.0.0.1:8000
Cite this entry
@misc{vaitp:cve202644502,
title = {{URL parsing mismatch in Bugsink's webhooks leads to an SSRF vulnerability.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-44502},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-44502/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
