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.
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>, CSSurl(...)/@import) and honors thehttp,https, andfileURL schemes. The HTML is built from the attacker-controlled uploaded file (e.g. an.eml's HTML body), and the fragment sanitizer does not stripimgorstyle. 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/urlimport endpoint, fixed in 1.3.0). That fix hardened only theHttpDownloaderclass; the WeasyPrint conversion path never goes throughHttpDownloader, so it has no IP validation and additionally allowsfile://. Confirmed live and unpatched in a lab against the latest release (v2.0.0, WeasyPrint 68.1).Details
backend/converters/email_convert.py:HTML(string=html_content).write_pdf(output_file)is called with nourl_fetcher=argument, so WeasyPrint's default fetcher resolves and downloads referenced resources overhttp/https/filewith no host/IP restriction. The pandoc path (backend/converters/pypandoc_convert.py,--pdf-engine=weasyprint) shares the same sink.html_content. The fragment sanitizer_sanitize_html_fragmentremovesscript|iframe|object|embed|link|meta|audio|video|sourcebut notimgand notstyle, so remote references in those elements survive into the render.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.validate_public_url()tobackend/downloaders/http_downloader.py(resolve host, reject non-public IPs, HTTP/HTTPS only). The WeasyPrint fetcher bypassesHttpDownloaderentirely, so none of that applies here, andfile://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:
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_fetcheron everyweasyprint.HTML(...).write_pdf(...)call (both the email and pandoc converters) that: allows onlyhttp/https(rejectfile://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 toHttpDownloader.validate_public_url()for CVE-2026-54054. Additionally, stripimg/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.