Skip to content

Commit 144aeda

Browse files
authored
fix(sdk): Fix possible panic for AttributeValueFQN.Prefix() (#1472)
This commit adds additional checks to validate that Attribute Name / Values are not empty or contain a slash. This fixes a panic condition where `NewAttributeValueFQN` would return without error but `Prefix()` would then fail when the prefix is attempted to be parsed by `NewAttributeNameFQN`. This commit includes unit testing, and the fuzz testing that initially discovered this issue. See there for possibly dangerous values. An alternative or additional solution that we may want to consider: If `Prefix()` is a common or frequent operation we could proactively invoke the `NewAttributeNameFQN` from within `NewAttributeValueFQN`. That way any future validation added to `NewAttributeNameFQN` would be proactively checked rather than needing `Prefix()` to be invoked to be tested. Also included are some minor doc changes, spelling fixes, and some other fuzz testing that was co-located with these changes. PR closes #1468
1 parent 32c09c3 commit 144aeda

File tree

3 files changed

+139
-4
lines changed

3 files changed

+139
-4
lines changed

sdk/fuzz_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ package sdk
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/base64"
67
"io"
8+
"net/http"
79
"testing"
810

11+
"github.com/lestrrat-go/jwx/v2/jwk"
12+
"github.com/opentdf/platform/sdk/auth"
13+
914
"github.com/opentdf/platform/lib/ocrypto"
1015
"github.com/stretchr/testify/assert"
1116
"github.com/stretchr/testify/require"
@@ -25,10 +30,22 @@ func newSDK() *SDK {
2530
sdk := &SDK{
2631
config: *cfg,
2732
kasKeyCache: newKasKeyCache(),
33+
tokenSource: &fakeTokenSource{},
2834
}
2935
return sdk
3036
}
3137

38+
type fakeTokenSource struct {
39+
}
40+
41+
func (f *fakeTokenSource) AccessToken(_ context.Context, _ *http.Client) (auth.AccessToken, error) {
42+
return "fake token", nil
43+
}
44+
45+
func (f *fakeTokenSource) MakeToken(func(jwk.Key) ([]byte, error)) ([]byte, error) {
46+
return []byte("fake token"), nil
47+
}
48+
3249
func unverifiedBase64Bytes(str string) []byte {
3350
b, _ := base64.StdEncoding.DecodeString(str)
3451
return b
@@ -92,6 +109,48 @@ func FuzzLoadTDF(f *testing.F) {
92109
})
93110
}
94111

112+
func FuzzReadNanoTDF(f *testing.F) {
113+
sdk := newSDK()
114+
f.Add([]byte{ // seed from xtest
115+
// header
116+
0x4c, 0x31, 0x4c, // version
117+
0x00, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0x3a, 0x38, 0x30, 0x38, 0x30, 0x2f, 0x6b, 0x61, 0x73, // kas
118+
0x00, // binding_mode
119+
0x01, // symmetric_and_payload_config
120+
// policy
121+
0x02, 0x00, 0x68, 0xef, 0x70, 0x7b, 0x5f, 0x20, 0xb9, 0x0b, 0xf5, 0x96, 0xc3, 0xd7, 0x42, 0x85, 0x17, 0x6c, 0xd8, 0x98,
122+
0xad, 0x47, 0xc4, 0x9a, 0x81, 0x5f, 0x67, 0xc4, 0x0f, 0xff, 0x16, 0xbb, 0xf0, 0xf4, 0xcd, 0x31, 0xa5, 0xf6, 0x86, 0x59,
123+
0x3d, 0xf1, 0x53, 0x39, 0x3c, 0x3e, 0x16, 0xd8, 0xd2, 0x3b, 0x37, 0x50, 0x86, 0x6c, 0xfd, 0x2b, 0xce, 0xc7, 0x10, 0x89,
124+
0x66, 0x74, 0x22, 0xf0, 0x3f, 0x16, 0x7a, 0xed, 0x37, 0x93, 0x03, 0x30, 0xcc, 0x05, 0x21, 0xd2, 0x9e, 0x5d, 0xc3, 0x34,
125+
0xc5, 0x51, 0x60, 0xe6, 0xbf, 0x16, 0xdf, 0x92, 0xd0, 0x8d, 0xb0, 0xf0, 0x57, 0x6f, 0x7c, 0x37, 0xb9, 0x84, 0x44, 0xc7,
126+
0x64, 0x99, 0x6a, 0xd3, 0x6e, 0xaa, 0x04,
127+
0xf3, 0x18, 0xe9, 0x0b, 0xd0, 0xdc, 0x05, 0x38, // gmac_binding
128+
// ephemeral_key
129+
0x03, 0x66, 0x95, 0xd9, 0x3b, 0x84, 0xee, 0xc5, 0x65, 0xc1, 0x13, 0x1c, 0x94, 0xc6, 0x00, 0x8b, 0xcb, 0x6a, 0xf5, 0x90,
130+
0xd5, 0x0d, 0x90, 0xc5, 0xf4, 0xe5, 0x96, 0x56, 0xb2, 0xd9, 0x4a, 0x9b, 0x51,
131+
// payload
132+
0x00, 0x00, 0x8f, // length
133+
0x54, 0x2b, 0x53, // iv
134+
// ciphertext
135+
0xce, 0x35, 0x1d, 0x0a, 0xd9, 0x7a, 0x81, 0xb5, 0xda, 0x93, 0x39, 0xd5, 0xa2, 0x42, 0x22, 0xa3, 0x64, 0x97, 0x2e, 0x33,
136+
0x41, 0x84, 0x12, 0x26, 0x81, 0xf5, 0x10, 0xc9, 0xf4, 0x94, 0xb8, 0x55, 0x52, 0x24, 0xeb, 0xaf, 0x89, 0xc3, 0x24, 0x7e,
137+
0x32, 0xcf, 0xd5, 0xda, 0xa2, 0xcb, 0x98, 0x67, 0x71, 0xc3, 0xa5, 0xf6, 0xa8, 0xe3, 0x4e, 0x64, 0x23, 0x2e, 0x40, 0xee,
138+
0x2e, 0xd9, 0xa4, 0x97, 0x87, 0x83, 0xd4, 0xe7, 0x11, 0xfe, 0xdb, 0xf4, 0x42, 0xc1, 0x71, 0x3b, 0x5a, 0x07, 0x01, 0x76,
139+
0xb2, 0xf8, 0x48, 0x23, 0x2d, 0xb3, 0x53, 0x61, 0x98, 0x39, 0x13, 0x7b, 0x45, 0xcd, 0x55, 0x76, 0xbe, 0x71, 0x3a, 0x88,
140+
0xf3, 0xce, 0xec, 0xc2, 0x68, 0x7d, 0xfd, 0x38, 0x4d, 0x49, 0xef, 0x57, 0x9a, 0xc7, 0x45, 0x81, 0xe4, 0x6f, 0xab, 0x4b,
141+
0x50, 0xa2, 0x43, 0x08, 0x71, 0x78, 0x43, 0xa2,
142+
0x66, 0x8e, 0x2b, 0xfd, 0x64, 0xc3, 0xed, 0x09, 0x1f, 0xa6, 0xe8, 0xa2, // mac
143+
})
144+
145+
f.Fuzz(func(t *testing.T, data []byte) {
146+
writer := bytes.NewBuffer(nil)
147+
_, err := sdk.ReadNanoTDF(writer, bytes.NewReader(data))
148+
149+
require.Error(t, err) // will always err due to no server running
150+
require.Equal(t, 0, writer.Len())
151+
})
152+
}
153+
95154
func FuzzReadPolicyBody(f *testing.F) {
96155
pb := &PolicyBody{
97156
mode: 0,
@@ -138,3 +197,55 @@ func FuzzNewResourceLocatorFromReader(f *testing.F) {
138197
require.NotNil(t, r)
139198
})
140199
}
200+
201+
var attributeSeeds = []string{
202+
// select seeds taken from granter_test.go
203+
"http://e/attr/a/value/1",
204+
"http://e/attr/1/value/one",
205+
"http://e/attr/value/value/one",
206+
"http://e/attr/a/value/%20",
207+
// error cases
208+
"http://e/attr",
209+
"hxxp://e/attr/a",
210+
"https://a/attr/%😁",
211+
"http://e/attr/a/value/b",
212+
// fuzzer discovered panic cases
213+
"http://0/attr//0/value/",
214+
"http://e/attr///value/0",
215+
"http://e/attr////value/0",
216+
"http://0/attr/0//value/0",
217+
"http://0/attr//0/value/0",
218+
"http://0/attr/0/0/value/0",
219+
}
220+
221+
func FuzzNewAttributeNameFQN(f *testing.F) {
222+
for _, s := range attributeSeeds {
223+
f.Add(s)
224+
}
225+
226+
f.Fuzz(func(_ *testing.T, data string) {
227+
fqn, err := NewAttributeNameFQN(data)
228+
if err == nil { // if possible validate additional functionality does not result in failure
229+
_ = fqn.Authority()
230+
_ = fqn.Name()
231+
_ = fqn.Prefix()
232+
_ = fqn.String()
233+
}
234+
})
235+
}
236+
237+
func FuzzNewAttributeValueFQN(f *testing.F) {
238+
for _, s := range attributeSeeds {
239+
f.Add(s)
240+
}
241+
242+
f.Fuzz(func(_ *testing.T, data string) {
243+
fqn, err := NewAttributeValueFQN(data)
244+
if err == nil {
245+
_ = fqn.Authority()
246+
_ = fqn.Name()
247+
_ = fqn.Prefix()
248+
_ = fqn.String()
249+
}
250+
})
251+
}

sdk/granter.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,28 @@ const (
2727
emptyTerm = "DEFAULT"
2828
)
2929

30-
// Represents a which KAS a split with the associated ID should shared with.
30+
// keySplitStep represents a which KAS a split with the associated ID should be shared with.
3131
type keySplitStep struct {
3232
KAS, SplitID string
3333
}
3434

35-
// Utility type to represent an FQN for an attribute.
35+
// AttributeNameFQN is a utility type to represent an FQN for an attribute.
3636
type AttributeNameFQN struct {
3737
url, key string
3838
}
3939

40+
func attributeURLPartsValid(parts []string) error {
41+
for i, part := range parts {
42+
if part == "" {
43+
return fmt.Errorf("%w: empty url path parts are not allowed", ErrInvalid)
44+
} else if i > 1 && // skip first two parts as they will have protocol slashes
45+
strings.Contains(part, "/") {
46+
return fmt.Errorf("%w: slash not allowed in name or values", ErrInvalid)
47+
}
48+
}
49+
return nil
50+
}
51+
4052
func NewAttributeNameFQN(u string) (AttributeNameFQN, error) {
4153
re := regexp.MustCompile(`^(https?://[\w./-]+)/attr/([^/\s]*)$`)
4254
m := re.FindStringSubmatch(u)
@@ -48,6 +60,11 @@ func NewAttributeNameFQN(u string) (AttributeNameFQN, error) {
4860
if err != nil {
4961
return AttributeNameFQN{}, fmt.Errorf("%w: error in attribute name [%s]", ErrInvalid, m[2])
5062
}
63+
64+
if err := attributeURLPartsValid(m); err != nil {
65+
return AttributeNameFQN{}, err
66+
}
67+
5168
return AttributeNameFQN{u, strings.ToLower(u)}, nil
5269
}
5370

@@ -86,7 +103,7 @@ func (a AttributeNameFQN) Name() string {
86103
return v
87104
}
88105

89-
// Utility type to represent an FQN for an attribute value.
106+
// AttributeValueFQN is a utility type to represent an FQN for an attribute value.
90107
type AttributeValueFQN struct {
91108
url, key string
92109
}
@@ -107,6 +124,10 @@ func NewAttributeValueFQN(u string) (AttributeValueFQN, error) {
107124
return AttributeValueFQN{}, fmt.Errorf("%w: error in attribute value [%s]", ErrInvalid, m[3])
108125
}
109126

127+
if err := attributeURLPartsValid(m); err != nil {
128+
return AttributeValueFQN{}, err
129+
}
130+
110131
return AttributeValueFQN{u, strings.ToLower(u)}, nil
111132
}
112133

@@ -351,7 +372,7 @@ func (r granter) plan(defaultKas []string, genSplitID func() string) ([]keySplit
351372
k = k.reduce()
352373
l := k.Len()
353374
if l == 0 {
354-
// default behavior: split key accross all default kases
375+
// default behavior: split key across all default kases
355376
switch len(defaultKas) {
356377
case 0:
357378
return nil, fmt.Errorf("no default KAS specified; required for grantless plans")

sdk/granter_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ func TestAttributeValueFromMalformedURL(t *testing.T) {
290290
{"invalid prefix 1", "hxxp://e/attr/a/value/1"},
291291
{"invalid prefix 2", "e/attr/a/a/value/1"},
292292
{"bad encoding", "https://a/attr/emoji/value/%😁"},
293+
{"empty name", "http://e/attr//value/0"},
294+
{"slash name", "http://e/attr///value/0"},
295+
{"slash in name", "http://0/attr/0/0/value/0"},
293296
} {
294297
t.Run(tc.n, func(t *testing.T) {
295298
a, err := NewAttributeValueFQN(tc.u)

0 commit comments

Comments
 (0)