From 482cf6fc9babd3ab06f6606762aac10447222201 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Wed, 18 Dec 2024 16:01:18 +0100 Subject: [PATCH] plugin: restrict characters in plugin names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to ⬡-49016 for reporting this issue. Fixes GHSA-32gq-x56h-299c --- cmd/age/testdata/plugin.txt | 14 ++++++++++++++ plugin/client.go | 6 ++++++ plugin/encode.go | 25 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/cmd/age/testdata/plugin.txt b/cmd/age/testdata/plugin.txt index 01e3ed8e..95cc0294 100644 --- a/cmd/age/testdata/plugin.txt +++ b/cmd/age/testdata/plugin.txt @@ -10,6 +10,15 @@ age -d -i long-key.txt test.age cmp stdout input ! stderr . +# check that path separators are rejected +chmod 755 age-plugin-pwn/pwn +mkdir $TMPDIR/age-plugin-pwn +cp age-plugin-pwn/pwn $TMPDIR/age-plugin-pwn/pwn +! age -r age1pwn/pwn19gt89dfz input +! age -d -i pwn-identity.txt test.age +! age -d -j pwn/pwn test.age +! exists pwn + -- input -- test -- key.txt -- @@ -18,3 +27,8 @@ AGE-PLUGIN-TEST-10Q32NLXM age1test10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qj6rl8p -- long-key.txt -- AGE-PLUGIN-TEST-10PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7Q5U8SUD +-- pwn-identity.txt -- +AGE-PLUGIN-PWN/PWN-19GYK4WLY +-- age-plugin-pwn/pwn -- +#!/bin/sh +touch "$WORK/pwn" diff --git a/plugin/client.go b/plugin/client.go index 2b50989a..051ec40b 100644 --- a/plugin/client.go +++ b/plugin/client.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "time" exec "golang.org/x/sys/execabs" @@ -178,6 +179,9 @@ func NewIdentity(s string, ui *ClientUI) (*Identity, error) { func NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) { s := EncodeIdentity(name, nil) + if s == "" { + return nil, fmt.Errorf("invalid plugin name: %q", name) + } return &Identity{ name: name, encoding: s, ui: ui, }, nil @@ -390,6 +394,8 @@ func openClientConnection(name, protocol string) (*clientConnection, error) { path := "age-plugin-" + name if testOnlyPluginPath != "" { path = filepath.Join(testOnlyPluginPath, path) + } else if strings.ContainsRune(name, os.PathSeparator) { + return nil, fmt.Errorf("invalid plugin name: %q", name) } cmd := exec.Command(path, "--age-plugin="+protocol) diff --git a/plugin/encode.go b/plugin/encode.go index 5000708a..0a59fbe0 100644 --- a/plugin/encode.go +++ b/plugin/encode.go @@ -14,6 +14,9 @@ import ( // EncodeIdentity encodes a plugin identity string for a plugin with the given // name. If the name is invalid, it returns an empty string. func EncodeIdentity(name string, data []byte) string { + if !validPluginName(name) { + return "" + } s, _ := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", data) return s } @@ -30,12 +33,18 @@ func ParseIdentity(s string) (name string, data []byte, err error) { } name = strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-") name = strings.ToLower(name) + if !validPluginName(name) { + return "", nil, fmt.Errorf("invalid plugin name: %q", name) + } return name, data, nil } // EncodeRecipient encodes a plugin recipient string for a plugin with the given // name. If the name is invalid, it returns an empty string. func EncodeRecipient(name string, data []byte) string { + if !validPluginName(name) { + return "" + } s, _ := bech32.Encode("age1"+strings.ToLower(name), data) return s } @@ -51,5 +60,21 @@ func ParseRecipient(s string) (name string, data []byte, err error) { return "", nil, fmt.Errorf("not a plugin recipient: %v", err) } name = strings.TrimPrefix(hrp, "age1") + if !validPluginName(name) { + return "", nil, fmt.Errorf("invalid plugin name: %q", name) + } return name, data, nil } + +func validPluginName(name string) bool { + if name == "" { + return false + } + allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-._" + for _, r := range name { + if !strings.ContainsRune(allowed, r) { + return false + } + } + return true +}