mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-07 21:13:18 +01:00
fix(exiftool): reject metadata keys that collide with ExifTool options
This commit is contained in:
@@ -80,6 +80,37 @@ var dangerousTags = []string{
|
|||||||
"FilePermissions", // Writing this changes the file's permissions
|
"FilePermissions", // Writing this changes the file's permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// controlOptions lists ExifTool command-line option names that collide with a
|
||||||
|
// tag assignment. A metadata key of "csv" becomes the argv entry "-csv=value",
|
||||||
|
// which exiftool reads as its own option rather than as a tag, so the value
|
||||||
|
// becomes a filename exiftool opens. Only an unprefixed key can collide:
|
||||||
|
// "-XMP:csv=value" is unambiguously a tag.
|
||||||
|
//
|
||||||
|
// See https://exiftool.org/exiftool_pod.html.
|
||||||
|
var controlOptions = []string{
|
||||||
|
"api", "argfile", "charset", "common_args", "config", "csv", "diff",
|
||||||
|
"echo", "efile", "execute", "ext", "fileorder", "geotag", "geosync",
|
||||||
|
"htmldump", "if", "json", "lang", "listitem", "o", "out", "p", "php",
|
||||||
|
"require", "srcfile", "stay_open", "tagsfromfile", "textout", "use", "w",
|
||||||
|
"wm", "xmlformat",
|
||||||
|
}
|
||||||
|
|
||||||
|
// isControlOption reports whether an unprefixed key would reach exiftool as
|
||||||
|
// one of its own options instead of as a tag assignment.
|
||||||
|
func isControlOption(key string) bool {
|
||||||
|
if strings.Contains(key, ":") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range controlOptions {
|
||||||
|
if strings.EqualFold(key, option) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// isDangerousTag reports whether key matches one of the [dangerousTags]
|
// isDangerousTag reports whether key matches one of the [dangerousTags]
|
||||||
// after case-insensitive comparison with any group prefix stripped.
|
// after case-insensitive comparison with any group prefix stripped.
|
||||||
func isDangerousTag(key string) bool {
|
func isDangerousTag(key string) bool {
|
||||||
@@ -114,6 +145,9 @@ func buildExifToolWriteArgs(metadata map[string]any) ([]string, error) {
|
|||||||
if !safeKeyPattern.MatchString(key) {
|
if !safeKeyPattern.MatchString(key) {
|
||||||
return nil, fmt.Errorf("write PDF metadata with ExifTool: invalid metadata key %q: %w", key, gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
return nil, fmt.Errorf("write PDF metadata with ExifTool: invalid metadata key %q: %w", key, gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||||
}
|
}
|
||||||
|
if isControlOption(key) {
|
||||||
|
return nil, fmt.Errorf("write PDF metadata with ExifTool: metadata key %q is an ExifTool option, prefix it with a group such as %q: %w", key, "XMP:"+key, gotenberg.ErrPdfEngineMetadataValueNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
tag := key
|
tag := key
|
||||||
if key == "Trapped" {
|
if key == "Trapped" {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package exiftool
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -211,3 +212,48 @@ func TestSafeKeyPattern(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A metadata key that collides with an ExifTool option becomes a bare argv
|
||||||
|
// entry such as "-csv=/etc/passwd", which exiftool reads as its own option and
|
||||||
|
// treats the value as a filename to open.
|
||||||
|
func TestBuildExifToolWriteArgs_RejectsControlOptions(t *testing.T) {
|
||||||
|
for _, key := range []string{"csv", "CSV", "json", "geotag", "config", "tagsFromFile", "execute", "stay_open", "o", "w", "if", "p"} {
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
_, err := buildExifToolWriteArgs(map[string]any{key: "/etc/passwd"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("buildExifToolWriteArgs accepted the control option %q", key)
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMetadataValueNotSupported) {
|
||||||
|
t.Fatalf("error %v does not wrap ErrPdfEngineMetadataValueNotSupported", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A group prefix makes the key unambiguous, so it must still be accepted.
|
||||||
|
func TestBuildExifToolWriteArgs_AcceptsPrefixedOptionNames(t *testing.T) {
|
||||||
|
for _, key := range []string{"XMP:csv", "XMP-dc:json", "IPTC:p"} {
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
args, err := buildExifToolWriteArgs(map[string]any{key: "value"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildExifToolWriteArgs rejected the prefixed key %q: %v", key, err)
|
||||||
|
}
|
||||||
|
want := fmt.Sprintf("-%s=value", key)
|
||||||
|
if len(args) != 1 || args[0] != want {
|
||||||
|
t.Fatalf("args = %v, want [%s]", args, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordinary tags must be unaffected.
|
||||||
|
func TestBuildExifToolWriteArgs_AcceptsOrdinaryTags(t *testing.T) {
|
||||||
|
for _, key := range []string{"Author", "Title", "Subject", "Keywords", "Producer", "Creator"} {
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
_, err := buildExifToolWriteArgs(map[string]any{key: "value"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildExifToolWriteArgs rejected the ordinary tag %q: %v", key, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user