Skip to content

Commit 3552482

Browse files
Effi-SAkramBitar
authored andcommitted
Added trailing bytes guard to ASN.1 decoding
Signed-off-by: Effi-S <effi.szt@gmail.com>
1 parent d0f95d4 commit 3552482

3 files changed

Lines changed: 97 additions & 2 deletions

File tree

.github/workflows/nightly-fuzz.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ jobs:
111111
- name: storage-sql-table-names
112112
pkg: ./token/services/storage/db/sql/common
113113
func: FuzzGetTableNamesNoPanic
114+
- name: tokens-typed-token-unmarshal
115+
pkg: ./token/services/tokens
116+
func: FuzzUnmarshalTypedTokenNoPanic
117+
- name: tokens-typed-metadata-unmarshal
118+
pkg: ./token/services/tokens
119+
func: FuzzUnmarshalTypedMetadataNoPanic
114120
- name: driver-identity-configuration-unique-id
115121
pkg: ./token/driver
116122
func: FuzzIdentityConfigurationUniqueIDInjective

token/services/tokens/typed.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ func (i TypedToken) Bytes() ([]byte, error) {
3030
// UnmarshalTypedToken deserializes an ASN.1 encoded byte slice into a TypedToken structure.
3131
func UnmarshalTypedToken(token driver.Token) (*TypedToken, error) {
3232
si := &TypedToken{}
33-
_, err := asn1.Unmarshal(token, si)
33+
rest, err := asn1.Unmarshal(token, si)
3434
if err != nil {
3535
return nil, errors.Wrap(err, "failed to unmarshal to TypedToken")
3636
}
37+
if len(rest) != 0 {
38+
return nil, errors.Errorf("trailing bytes after TypedToken")
39+
}
3740

3841
return si, nil
3942
}
@@ -64,10 +67,13 @@ func (i TypedMetadata) Bytes() ([]byte, error) {
6467
// UnmarshalTypedMetadata deserializes an ASN.1 encoded byte slice into a TypedMetadata structure.
6568
func UnmarshalTypedMetadata(metadata driver.Metadata) (*TypedMetadata, error) {
6669
si := &TypedMetadata{}
67-
_, err := asn1.Unmarshal(metadata, si)
70+
rest, err := asn1.Unmarshal(metadata, si)
6871
if err != nil {
6972
return nil, errors.Wrap(err, "failed to unmarshal to TypedMetadata")
7073
}
74+
if len(rest) != 0 {
75+
return nil, errors.Errorf("trailing bytes after TypedMetadata")
76+
}
7177

7278
return si, nil
7379
}

token/services/tokens/typed_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"github.com/stretchr/testify/require"
1616
)
1717

18+
const maxFuzzTypedBytes = 256 << 10
19+
1820
func TestSerialization(t *testing.T) {
1921
raw := []byte("pineapple")
2022
wrappedToken, err := tokens.WrapWithType(0, raw)
@@ -24,3 +26,84 @@ func TestSerialization(t *testing.T) {
2426
assert.Equal(t, driver.Type(0), tok.Type)
2527
assert.Equal(t, driver.Token(raw), tok.Token)
2628
}
29+
30+
// TestUnmarshalTypedTokenRejectsTrailingBytes verifies that a valid encoding
31+
// with arbitrary junk appended is rejected rather than silently accepted. See
32+
// issue #2189: ignoring the ASN.1 `rest` slice makes the decoder malleable —
33+
// two distinct byte strings would otherwise decode to the same object.
34+
func TestUnmarshalTypedTokenRejectsTrailingBytes(t *testing.T) {
35+
valid, err := tokens.WrapWithType(7, driver.Token("payload"))
36+
require.NoError(t, err)
37+
38+
// Sanity: the clean encoding still decodes fine.
39+
_, err = tokens.UnmarshalTypedToken(valid)
40+
require.NoError(t, err)
41+
42+
withTrailer := append(append([]byte{}, valid...), 0x00, 0x01, 0x02)
43+
got, err := tokens.UnmarshalTypedToken(withTrailer)
44+
require.Error(t, err)
45+
require.Nil(t, got)
46+
require.Contains(t, err.Error(), "trailing bytes")
47+
}
48+
49+
// TestUnmarshalTypedMetadataRejectsTrailingBytes is the metadata counterpart of
50+
// TestUnmarshalTypedTokenRejectsTrailingBytes.
51+
func TestUnmarshalTypedMetadataRejectsTrailingBytes(t *testing.T) {
52+
valid, err := tokens.WrapMetadataWithType(7, driver.Metadata("payload"))
53+
require.NoError(t, err)
54+
55+
_, err = tokens.UnmarshalTypedMetadata(valid)
56+
require.NoError(t, err)
57+
58+
withTrailer := append(append([]byte{}, valid...), 0x00, 0x01, 0x02)
59+
got, err := tokens.UnmarshalTypedMetadata(withTrailer)
60+
require.Error(t, err)
61+
require.Nil(t, got)
62+
require.Contains(t, err.Error(), "trailing bytes")
63+
}
64+
65+
// FuzzUnmarshalTypedTokenNoPanic fuzzes UnmarshalTypedToken with arbitrary
66+
// bytes. This decoder sits on the receive path for untrusted, ledger-stored
67+
// and peer-supplied token bytes (see issue #2189), so any panic here is an
68+
// unauthenticated DoS against every caller that reads typed tokens.
69+
func FuzzUnmarshalTypedTokenNoPanic(f *testing.F) {
70+
valid, err := tokens.WrapWithType(7, driver.Token("payload"))
71+
require.NoError(f, err)
72+
73+
f.Add([]byte(valid))
74+
f.Add([]byte{})
75+
f.Add([]byte("malformed"))
76+
f.Add([]byte(valid)[:len(valid)/2])
77+
f.Add(append(append([]byte{}, valid...), 0x00, 0x01, 0x02))
78+
79+
f.Fuzz(func(t *testing.T, raw []byte) {
80+
if len(raw) > maxFuzzTypedBytes {
81+
t.Skip()
82+
}
83+
require.NotPanics(t, func() {
84+
_, _ = tokens.UnmarshalTypedToken(raw)
85+
})
86+
})
87+
}
88+
89+
// FuzzUnmarshalTypedMetadataNoPanic fuzzes UnmarshalTypedMetadata with
90+
// arbitrary bytes, for the same reasons as FuzzUnmarshalTypedTokenNoPanic.
91+
func FuzzUnmarshalTypedMetadataNoPanic(f *testing.F) {
92+
valid, err := tokens.WrapMetadataWithType(7, driver.Metadata("payload"))
93+
require.NoError(f, err)
94+
95+
f.Add([]byte(valid))
96+
f.Add([]byte{})
97+
f.Add([]byte("malformed"))
98+
f.Add([]byte(valid)[:len(valid)/2])
99+
f.Add(append(append([]byte{}, valid...), 0x00, 0x01, 0x02))
100+
101+
f.Fuzz(func(t *testing.T, raw []byte) {
102+
if len(raw) > maxFuzzTypedBytes {
103+
t.Skip()
104+
}
105+
require.NotPanics(t, func() {
106+
_, _ = tokens.UnmarshalTypedMetadata(raw)
107+
})
108+
})
109+
}

0 commit comments

Comments
 (0)