From 366cc58ca8ff9447bb67a57d77e0821e119c34f7 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sat, 29 Aug 2026 16:01:36 +0200 Subject: [PATCH] internal/bech32: avoid panic on Unicode input Reported by Joe Doyle of Trail of Bits. --- age_test.go | 28 ++++++++++++++++++++++++++++ internal/bech32/bech32.go | 9 ++++++++- internal/bech32/bech32_test.go | 20 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/age_test.go b/age_test.go index 7cded16..22e1579 100644 --- a/age_test.go +++ b/age_test.go @@ -18,6 +18,7 @@ import ( "testing" "filippo.io/age" + "filippo.io/age/plugin" ) func ExampleEncrypt() { @@ -549,3 +550,30 @@ func TestEncryptReader(t *testing.T) { t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld) } } + +func TestParseUnicode(t *testing.T) { + // U+212A folds to "k", shrinking the data part below the checksum. + w := "AA3100AC" + string(rune(0x212A)) + for _, tc := range []struct { + name string + fn func(string) error + }{ + {"age.ParseX25519Recipient", func(s string) error { _, err := age.ParseX25519Recipient(s); return err }}, + {"age.ParseX25519Identity", func(s string) error { _, err := age.ParseX25519Identity(s); return err }}, + {"age.ParseHybridRecipient", func(s string) error { _, err := age.ParseHybridRecipient(s); return err }}, + {"age.ParseHybridIdentity", func(s string) error { _, err := age.ParseHybridIdentity(s); return err }}, + {"plugin.ParseIdentity", func(s string) error { _, _, err := plugin.ParseIdentity(s); return err }}, + {"plugin.ParseRecipient", func(s string) error { _, _, err := plugin.ParseRecipient(s); return err }}, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("%s panicked on malformed input: %v", tc.name, r) + } + }() + if err := tc.fn(w); err == nil { + t.Errorf("%s returned nil error, want error", tc.name) + } + }) + } +} diff --git a/internal/bech32/bech32.go b/internal/bech32/bech32.go index a5d8330..087de3d 100644 --- a/internal/bech32/bech32.go +++ b/internal/bech32/bech32.go @@ -154,14 +154,21 @@ func Decode(s string) (hrp string, data []byte, err error) { return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c) } } - s = strings.ToLower(s) for p, c := range s[pos+1:] { + // Fold ASCII explicitly. Unicode case folding can turn a non-ASCII + // rune into a shorter valid charset member. + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } d := strings.IndexRune(charset, c) if d == -1 { return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c) } data = append(data, byte(d)) } + if len(data) < 6 { + return "", nil, fmt.Errorf("data part too short") + } if !verifyChecksum(hrp, data) { return "", nil, fmt.Errorf("invalid checksum") } diff --git a/internal/bech32/bech32_test.go b/internal/bech32/bech32_test.go index 01ce361..2738a3b 100644 --- a/internal/bech32/bech32_test.go +++ b/internal/bech32/bech32_test.go @@ -93,3 +93,23 @@ func TestBech32(t *testing.T) { } } } + +func TestDecodeShortDataPart(t *testing.T) { + kelvin := string(rune(0x212A)) + for _, s := range []string{ + "AA3100AC" + kelvin, + "BK1" + kelvin + "0JFM", + "AQM1KZCML", + } { + func() { + defer func() { + if r := recover(); r != nil { + t.Errorf("Decode(%+q) panicked: %v", s, r) + } + }() + if _, _, err := bech32.Decode(s); err == nil { + t.Errorf("Decode(%+q) = nil error, want error", s) + } + }() + } +}