Skip to content

SSRF (full-read) via WeasyPrint PDF rendering: uploaded document with remote/file resources makes the server fetch arbitrary internal URLs

Moderate
chase-roohms published GHSA-jcqc-6m64-4xv6 Sep 29, 2026

Package

transmute-app/transmute

Affected versions

<= 2.0.0

Patched versions

>= 2.1.0

Description

Summary

When Transmute converts an uploaded document to PDF, it renders HTML with WeasyPrint using WeasyPrint's default URL fetcher and registers no custom url_fetcher. WeasyPrint's default fetcher downloads any resource the HTML references (<img src>, CSS url(...) / @import) and honors the http, https, and file URL schemes. The HTML is built from the attacker-controlled uploaded file (e.g. an .eml's HTML body), and the fragment sanitizer does not strip img or style. As a result, any user who can convert a file to PDF can make the server issue arbitrary requests to internal services, cloud instance-metadata endpoints, and local files. Because WeasyPrint embeds the fetched resource into the output, internal content that WeasyPrint can render (images/SVG) is fully read back in the PDF the attacker downloads (full-read SSRF, not blind).

This is a different, unpatched sink from CVE-2026-54054 / GHSA-89m5-mvmx-7xrf (SSRF in the POST /api/files/url import endpoint, fixed in 1.3.0). That fix hardened only the HttpDownloader class; the WeasyPrint conversion path never goes through HttpDownloader, so it has no IP validation and additionally allows file://. Confirmed live and unpatched in a lab against the latest release (v2.0.0, WeasyPrint 68.1).

Details

  • Sink — backend/converters/email_convert.py: HTML(string=html_content).write_pdf(output_file) is called with no url_fetcher= argument, so WeasyPrint's default fetcher resolves and downloads referenced resources over http/https/file with no host/IP restriction. The pandoc path (backend/converters/pypandoc_convert.py, --pdf-engine=weasyprint) shares the same sink.
  • Attacker input reaches the sink — the email's HTML body is included in html_content. The fragment sanitizer _sanitize_html_fragment removes script|iframe|object|embed|link|meta|audio|video|source but not img and not style, so remote references in those elements survive into the render.
  • Reachability — conversion is the core feature, and the upload/convert/download endpoints are gated only by get_current_active_user (any authenticated user). When guest access is enabled (ALLOW_UNAUTHENTICATED=true), a guest session mints a token with no credentials, making the whole chain unauthenticated.
  • Not covered by the CVE-2026-54054 fix — that fix added validate_public_url() to backend/downloaders/http_downloader.py (resolve host, reject non-public IPs, HTTP/HTTPS only). The WeasyPrint fetcher bypasses HttpDownloader entirely, so none of that applies here, and file:// is additionally reachable.

Affected versions: all releases through the current v2.0.0.

Verification

Confirmed in an isolated lab (v2.0.0, WeasyPrint 68.1) against an internal-only service not reachable from outside the container network. Converting an uploaded document that referenced that service caused the requests to arrive from the app container, and content the renderer could rasterize was recovered verbatim from the resulting PDF, confirming full-read rather than blind SSRF. The file:// scheme was separately confirmed to be honored.

Impact

Server-Side Request Forgery (CWE-918). Any user who can convert a file to PDF — and any unauthenticated visitor when guest access is enabled — can make the server:

  • read internal-only HTTP services and cloud instance metadata, with full-read exfiltration into the downloadable PDF for any resource WeasyPrint can render (images/SVG) and a blind request + open/closed-port oracle for the rest;
  • access local resources via the file:// scheme.

The sibling import-path SSRF in this same project was assigned CVE-2026-54054 (6.5); this sink is distinct, unpatched, reachable at lower privilege (unauth in guest mode), and additionally supports file://.

Remediation

Register a hardened url_fetcher on every weasyprint.HTML(...).write_pdf(...) call (both the email and pandoc converters) that: allows only http/https (reject file:// and all other schemes), resolves the host and rejects private/loopback/link-local/reserved/CGNAT/cloud-metadata ranges, pins the connection to the validated IP (DNS-rebinding-safe), and validates every redirect hop — i.e. reuse the guard already added to HttpDownloader.validate_public_url() for CVE-2026-54054. Additionally, strip img/style (or disable remote-resource loading) in _sanitize_html_fragment. For an anonymous-conversion deployment, remote resource fetching during rendering should be off by default.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits