GHSA-9pmc-p236-855h: Ruby CSS Parser: SSRF and Local File Disclosure in `CssParser::Parser#read_remote_file`
## Summary `CssParser::Parser#read_remote_file` (and therefore `load_uri!`, and the `@import`-following branch of `add_block!`) issues HTTP/HTTPS requests against any host, port and URI it is handed, with no scheme allowlist, no host / IP filtering, and no protection against link-local, loopback or RFC‑1918 addresses. `Location:` redirects are followed recursively back into the same function, which **also services `file://` URIs**, so a single attacker-controlled HTTP redirect upgrades the bug from SSRF to arbitrary local file disclosure. In practice, any consumer of `css_parser` that hands it attacker‑influenced CSS together with a `base_uri:` option — Premailer being the canonical example — is exposed. The attacker only needs the ability to land one `@import url(...)` in the CSS that the host application parses. ## Vulnerable code [`lib/css_parser/parser.rb#L613-L687`](https://github.com/premailer/css_parser/blob/cee10f16b11dd175f17b0e5f6aefa488378e964b/lib/css_parser/parser.rb#L613-L687): ```ruby def read_remote_file(uri) # :nodoc: ... begin uri = Addressable::URI.parse(uri.to_s) if uri.scheme == 'file' # local file path = uri.path path.gsub!(%r{^/}, '') if Gem.win_platform? src = File.read(path, mode: 'rb') # <-- arbitrary local read else # remote file if uri.scheme == 'https' uri.port = 443 unless uri.port http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true else http = Net::HTTP.new(uri.host, uri.port) # <-- arbitrary host:port end res = http.get(uri.request_uri, ...) ... elsif res.code.to_i >= 300 and res.code.to_i < 400 unless res['Location'].nil? return read_remote_file Addressable::URI.parse(...) # <-- cross-scheme redirect end end ... ``` There is **no validation** of `uri.host`, `uri.scheme`, or the resolved IP address — neither before the HTTP request, nor before the recursive call on the `Location` header. The user-facing entry points that reach this sink are: - [`Parser#load_uri!`](https://github.com/premailer/css_parser/blob/cee10f16b11dd175f17b0e5f6aefa488378e964b/lib/css_parser/parser.rb#L489-L516) — directly calls `read_remote_file` (line 513). - [`Parser#add_block!`](https://github.com/premailer/css_parser/blob/cee10f16b11dd175f17b0e5f6aefa488378e964b/lib/css_parser/parser.rb#L117-L162) — when invoked with `base_uri:` and the default `import: true`, scans the CSS for `@import url(...)` rules and resolves each one through `load_uri!` (line 150). ## Threat model and scope | Capability | Reachable? | Notes | |---|---|---| | Arbitrary outbound `http://` / `https://` GET to any host:port reachable from the server | ✅ | No host, IP, port or scheme allowlist. Works against loopback, RFC‑1918, link‑local (169.254/16, AWS / GCP / Azure IMDS), and Docker / k8s service IPs. | | Recovering the response body | ✅ (conditional) | The body is fed back into `add_block!`. Any bytes that form `selector { decl }` round‑trip out via `Parser#each_selector` / `to_s`. A consumer that emits the parsed CSS (e.g. Premailer inlining into HTML) leaks the body to the original attacker. | | Local file read via cross-scheme redirect (`Location: file://...`) | ⚠️ partial | Redirect is followed without checking the new scheme; the recursive call services `file://` with `File.read`. The read itself executes against any path the Ruby process can open. **Content recovery via the parser API is constrained by CSS grammar** — see "File-disclosure scope" below. | | File-existence oracle | ✅ | With default `io_exceptions: true`, missing paths raise `CssParser::RemoteFileError` and existing paths return silently. An attacker can iterate `file://` targets to enumerate filesystem layout, usernames, installed software, etc. | | Side-effecting GETs against unprotected internal admin endpoints | ✅ | Even with a non‑CSS response, the request still fires. Internal services that act on GET (legacy admin panels, debug endpoints, cache busters, queue triggers) execute. | | Forced gzip / deflate decompression (DoS / decompression bomb) | ✅ | `Accept-Encoding: gzip` is hardcoded (line 650) and the body is decompressed with `Zlib::GzipReader` / `Zlib::Inflate` (lines 665-672). An attacker server can return a small gzipped payload that decompresses to multiple GB. | | HTTPS internal targets with self-signed / private CA certs | ⚠️ | `Net::HTTP#use_ssl = true` defaults to `VERIFY_PEER`, so internal HTTPS hosts with private certs will fail the SSL handshake. Plain HTTP, and HTTPS hosts whose certs chain to a CA trusted by the process, are fully exploitable. | | Smuggling other TCP protocols (Redis, Memcached, etc.) via the HTTP client | ⚠️ | `Net::HTTP` writes a real HTTP request line, so Redis / Memcached won't accept it. The TCP connect itself does occur, which can still trigger logs, port-scan effects, and connection-state side effects in firewalled environments. |
GHSA-9pmc-p236-855h: Ruby CSS Parser: SSRF and Local File Disclosure in `CssParser::Parser#read_remote_file`
Description
## Summary `CssParser::Parser#read_remote_file` (and therefore `load_uri!`, and the `@import`-following branch of `add_block!`) issues HTTP/HTTPS requests against any host, port and URI it is handed, with no scheme allowlist, no host / IP filtering, and no protection against link-local, loopback or RFC‑1918 addresses. `Location:` redirects are followed recursively back into the same function, which **also services `file://` URIs**, so a single attacker-controlled HTTP redirect upgrades the bug from SSRF to arbitrary local file disclosure. In practice, any consumer of `css_parser` that hands it attacker‑influenced CSS together with a `base_uri:` option — Premailer being the canonical example — is exposed. The attacker only needs the ability to land one `@import url(...)` in the CSS that the host application parses. ## Vulnerable code [`lib/css_parser/parser.rb#L613-L687`](https://github.com/premailer/css_parser/blob/cee10f16b11dd175f17b0e5f6aefa488378e964b/lib/css_parser/parser.rb#L613-L687): ```ruby def read_remote_file(uri) # :nodoc: ... begin uri = Addressable::URI.parse(uri.to_s) if uri.scheme == 'file' # local file path = uri.path path.gsub!(%r{^/}, '') if Gem.win_platform? src = File.read(path, mode: 'rb') # <-- arbitrary local read else # remote file if uri.scheme == 'https' uri.port = 443 unless uri.port http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true else http = Net::HTTP.new(uri.host, uri.port) # <-- arbitrary host:port end res = http.get(uri.request_uri, ...) ... elsif res.code.to_i >= 300 and res.code.to_i < 400 unless res['Location'].nil? return read_remote_file Addressable::URI.parse(...) # <-- cross-scheme redirect end end ... ``` There is **no validation** of `uri.host`, `uri.scheme`, or the resolved IP address — neither before the HTTP request, nor before the recursive call on the `Location` header. The user-facing entry points that reach this sink are: - [`Parser#load_uri!`](https://github.com/premailer/css_parser/blob/cee10f16b11dd175f17b0e5f6aefa488378e964b/lib/css_parser/parser.rb#L489-L516) — directly calls `read_remote_file` (line 513). - [`Parser#add_block!`](https://github.com/premailer/css_parser/blob/cee10f16b11dd175f17b0e5f6aefa488378e964b/lib/css_parser/parser.rb#L117-L162) — when invoked with `base_uri:` and the default `import: true`, scans the CSS for `@import url(...)` rules and resolves each one through `load_uri!` (line 150). ## Threat model and scope | Capability | Reachable? | Notes | |---|---|---| | Arbitrary outbound `http://` / `https://` GET to any host:port reachable from the server | ✅ | No host, IP, port or scheme allowlist. Works against loopback, RFC‑1918, link‑local (169.254/16, AWS / GCP / Azure IMDS), and Docker / k8s service IPs. | | Recovering the response body | ✅ (conditional) | The body is fed back into `add_block!`. Any bytes that form `selector { decl }` round‑trip out via `Parser#each_selector` / `to_s`. A consumer that emits the parsed CSS (e.g. Premailer inlining into HTML) leaks the body to the original attacker. | | Local file read via cross-scheme redirect (`Location: file://...`) | ⚠️ partial | Redirect is followed without checking the new scheme; the recursive call services `file://` with `File.read`. The read itself executes against any path the Ruby process can open. **Content recovery via the parser API is constrained by CSS grammar** — see "File-disclosure scope" below. | | File-existence oracle | ✅ | With default `io_exceptions: true`, missing paths raise `CssParser::RemoteFileError` and existing paths return silently. An attacker can iterate `file://` targets to enumerate filesystem layout, usernames, installed software, etc. | | Side-effecting GETs against unprotected internal admin endpoints | ✅ | Even with a non‑CSS response, the request still fires. Internal services that act on GET (legacy admin panels, debug endpoints, cache busters, queue triggers) execute. | | Forced gzip / deflate decompression (DoS / decompression bomb) | ✅ | `Accept-Encoding: gzip` is hardcoded (line 650) and the body is decompressed with `Zlib::GzipReader` / `Zlib::Inflate` (lines 665-672). An attacker server can return a small gzipped payload that decompresses to multiple GB. | | HTTPS internal targets with self-signed / private CA certs | ⚠️ | `Net::HTTP#use_ssl = true` defaults to `VERIFY_PEER`, so internal HTTPS hosts with private certs will fail the SSL handshake. Plain HTTP, and HTTPS hosts whose certs chain to a CA trusted by the process, are fully exploitable. | | Smuggling other TCP protocols (Redis, Memcached, etc.) via the HTTP client | ⚠️ | `Net::HTTP` writes a real HTTP request line, so Redis / Memcached won't accept it. The TCP connect itself does occur, which can still trigger logs, port-scan effects, and connection-state side effects in firewalled environments. |
CVSS v4.0
Affected software
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
Weaknesses
Technical Details
- Gcve Source
- db.gcve.eu
- Osv Id
- GHSA-9pmc-p236-855h
- Osv Schema Version
- 1.4.0
- Aliases
- ["CVE-2026-53727"]
- Ecosystems
- ["RubyGems"]
- Database Specific Severity
- HIGH
- Cvss Version
- 4.0
Threat ID: 6a50bac868715ace435892a4
Added to database: 07/10/2026, 09:26:32 UTC
Last updated: 07/10/2026, 09:26:32 UTC
Views: 1
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.