internal/bech32: avoid panic on Unicode input

Reported by Joe Doyle of Trail of Bits.
This commit is contained in:
Filippo Valsorda
2026-08-29 19:30:10 +02:00
parent 7a8f500567
commit 366cc58ca8
3 changed files with 56 additions and 1 deletions
+28
View File
@@ -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)
}
})
}
}
+8 -1
View File
@@ -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")
}
+20
View File
@@ -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)
}
}()
}
}