Skip to content

Commit 20e3df6

Browse files
tallclairk8s-publishing-bot
authored andcommitted
Validate etcd paths
Kubernetes-commit: 6775c99cd008c457ce3eed401ac1c60c3812dbfa
1 parent c19c82e commit 20e3df6

File tree

4 files changed

+218
-89
lines changed

4 files changed

+218
-89
lines changed

pkg/storage/etcd3/linearized_read_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ func TestLinearizedReadRevisionInvariant(t *testing.T) {
3737
// [1] https://etcd.io/docs/v3.5/learning/api_guarantees/#isolation-level-and-consistency-of-replicas
3838
ctx, store, etcdClient := testSetup(t)
3939

40-
key := "/testkey"
40+
dir := "/testing"
41+
key := dir + "/testkey"
4142
out := &example.Pod{}
4243
obj := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", SelfLink: "testlink"}}
4344

@@ -53,7 +54,7 @@ func TestLinearizedReadRevisionInvariant(t *testing.T) {
5354
}
5455

5556
list := &example.PodList{}
56-
if err := store.GetList(ctx, "/", storage.ListOptions{Predicate: storage.Everything, Recursive: true}, list); err != nil {
57+
if err := store.GetList(ctx, dir, storage.ListOptions{Predicate: storage.Everything, Recursive: true}, list); err != nil {
5758
t.Errorf("Unexpected List error: %v", err)
5859
}
5960
finalRevision := list.ResourceVersion

pkg/storage/etcd3/store.go

+94-44
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,21 @@ func New(c *clientv3.Client, codec runtime.Codec, newFunc func() runtime.Object,
9898

9999
func newStore(c *clientv3.Client, codec runtime.Codec, newFunc func() runtime.Object, prefix string, groupResource schema.GroupResource, transformer value.Transformer, pagingEnabled bool, leaseManagerConfig LeaseManagerConfig) *store {
100100
versioner := storage.APIObjectVersioner{}
101+
// for compatibility with etcd2 impl.
102+
// no-op for default prefix of '/registry'.
103+
// keeps compatibility with etcd2 impl for custom prefixes that don't start with '/'
104+
pathPrefix := path.Join("/", prefix)
105+
if !strings.HasSuffix(pathPrefix, "/") {
106+
// Ensure the pathPrefix ends in "/" here to simplify key concatenation later.
107+
pathPrefix += "/"
108+
}
101109
result := &store{
102-
client: c,
103-
codec: codec,
104-
versioner: versioner,
105-
transformer: transformer,
106-
pagingEnabled: pagingEnabled,
107-
// for compatibility with etcd2 impl.
108-
// no-op for default prefix of '/registry'.
109-
// keeps compatibility with etcd2 impl for custom prefixes that don't start with '/'
110-
pathPrefix: path.Join("/", prefix),
110+
client: c,
111+
codec: codec,
112+
versioner: versioner,
113+
transformer: transformer,
114+
pagingEnabled: pagingEnabled,
115+
pathPrefix: pathPrefix,
111116
groupResource: groupResource,
112117
groupResourceString: groupResource.String(),
113118
watcher: newWatcher(c, codec, newFunc, versioner, transformer),
@@ -123,9 +128,12 @@ func (s *store) Versioner() storage.Versioner {
123128

124129
// Get implements storage.Interface.Get.
125130
func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, out runtime.Object) error {
126-
key = path.Join(s.pathPrefix, key)
131+
preparedKey, err := s.prepareKey(key)
132+
if err != nil {
133+
return err
134+
}
127135
startTime := time.Now()
128-
getResp, err := s.client.KV.Get(ctx, key)
136+
getResp, err := s.client.KV.Get(ctx, preparedKey)
129137
metrics.RecordEtcdRequestLatency("get", getTypeName(out), startTime)
130138
if err != nil {
131139
return err
@@ -138,11 +146,11 @@ func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, ou
138146
if opts.IgnoreNotFound {
139147
return runtime.SetZeroValue(out)
140148
}
141-
return storage.NewKeyNotFoundError(key, 0)
149+
return storage.NewKeyNotFoundError(preparedKey, 0)
142150
}
143151
kv := getResp.Kvs[0]
144152

145-
data, _, err := s.transformer.TransformFromStorage(ctx, kv.Value, authenticatedDataString(key))
153+
data, _, err := s.transformer.TransformFromStorage(ctx, kv.Value, authenticatedDataString(preparedKey))
146154
if err != nil {
147155
return storage.NewInternalError(err.Error())
148156
}
@@ -152,6 +160,10 @@ func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, ou
152160

153161
// Create implements storage.Interface.Create.
154162
func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error {
163+
preparedKey, err := s.prepareKey(key)
164+
if err != nil {
165+
return err
166+
}
155167
trace := utiltrace.New("Create etcd3",
156168
utiltrace.Field{"audit-id", endpointsrequest.GetAuditIDTruncated(ctx)},
157169
utiltrace.Field{"key", key},
@@ -170,24 +182,23 @@ func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object,
170182
if err != nil {
171183
return err
172184
}
173-
key = path.Join(s.pathPrefix, key)
174185

175186
opts, err := s.ttlOpts(ctx, int64(ttl))
176187
if err != nil {
177188
return err
178189
}
179190

180-
newData, err := s.transformer.TransformToStorage(ctx, data, authenticatedDataString(key))
191+
newData, err := s.transformer.TransformToStorage(ctx, data, authenticatedDataString(preparedKey))
181192
trace.Step("TransformToStorage finished", utiltrace.Field{"err", err})
182193
if err != nil {
183194
return storage.NewInternalError(err.Error())
184195
}
185196

186197
startTime := time.Now()
187198
txnResp, err := s.client.KV.Txn(ctx).If(
188-
notFound(key),
199+
notFound(preparedKey),
189200
).Then(
190-
clientv3.OpPut(key, string(newData), opts...),
201+
clientv3.OpPut(preparedKey, string(newData), opts...),
191202
).Commit()
192203
metrics.RecordEtcdRequestLatency("create", getTypeName(obj), startTime)
193204
trace.Step("Txn call finished", utiltrace.Field{"err", err})
@@ -196,7 +207,7 @@ func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object,
196207
}
197208

198209
if !txnResp.Succeeded {
199-
return storage.NewKeyExistsError(key, 0)
210+
return storage.NewKeyExistsError(preparedKey, 0)
200211
}
201212

202213
if out != nil {
@@ -212,12 +223,15 @@ func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object,
212223
func (s *store) Delete(
213224
ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions,
214225
validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object) error {
226+
preparedKey, err := s.prepareKey(key)
227+
if err != nil {
228+
return err
229+
}
215230
v, err := conversion.EnforcePtr(out)
216231
if err != nil {
217232
return fmt.Errorf("unable to convert output object to pointer: %v", err)
218233
}
219-
key = path.Join(s.pathPrefix, key)
220-
return s.conditionalDelete(ctx, key, out, v, preconditions, validateDeletion, cachedExistingObject)
234+
return s.conditionalDelete(ctx, preparedKey, out, v, preconditions, validateDeletion, cachedExistingObject)
221235
}
222236

223237
func (s *store) conditionalDelete(
@@ -330,6 +344,10 @@ func (s *store) conditionalDelete(
330344
func (s *store) GuaranteedUpdate(
331345
ctx context.Context, key string, destination runtime.Object, ignoreNotFound bool,
332346
preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object) error {
347+
preparedKey, err := s.prepareKey(key)
348+
if err != nil {
349+
return err
350+
}
333351
trace := utiltrace.New("GuaranteedUpdate etcd3",
334352
utiltrace.Field{"audit-id", endpointsrequest.GetAuditIDTruncated(ctx)},
335353
utiltrace.Field{"key", key},
@@ -340,16 +358,15 @@ func (s *store) GuaranteedUpdate(
340358
if err != nil {
341359
return fmt.Errorf("unable to convert output object to pointer: %v", err)
342360
}
343-
key = path.Join(s.pathPrefix, key)
344361

345362
getCurrentState := func() (*objState, error) {
346363
startTime := time.Now()
347-
getResp, err := s.client.KV.Get(ctx, key)
364+
getResp, err := s.client.KV.Get(ctx, preparedKey)
348365
metrics.RecordEtcdRequestLatency("get", getTypeName(destination), startTime)
349366
if err != nil {
350367
return nil, err
351368
}
352-
return s.getState(ctx, getResp, key, v, ignoreNotFound)
369+
return s.getState(ctx, getResp, preparedKey, v, ignoreNotFound)
353370
}
354371

355372
var origState *objState
@@ -365,9 +382,9 @@ func (s *store) GuaranteedUpdate(
365382
}
366383
trace.Step("initial value restored")
367384

368-
transformContext := authenticatedDataString(key)
385+
transformContext := authenticatedDataString(preparedKey)
369386
for {
370-
if err := preconditions.Check(key, origState.obj); err != nil {
387+
if err := preconditions.Check(preparedKey, origState.obj); err != nil {
371388
// If our data is already up to date, return the error
372389
if origStateIsCurrent {
373390
return err
@@ -453,11 +470,11 @@ func (s *store) GuaranteedUpdate(
453470

454471
startTime := time.Now()
455472
txnResp, err := s.client.KV.Txn(ctx).If(
456-
clientv3.Compare(clientv3.ModRevision(key), "=", origState.rev),
473+
clientv3.Compare(clientv3.ModRevision(preparedKey), "=", origState.rev),
457474
).Then(
458-
clientv3.OpPut(key, string(newData), opts...),
475+
clientv3.OpPut(preparedKey, string(newData), opts...),
459476
).Else(
460-
clientv3.OpGet(key),
477+
clientv3.OpGet(preparedKey),
461478
).Commit()
462479
metrics.RecordEtcdRequestLatency("update", getTypeName(destination), startTime)
463480
trace.Step("Txn call finished", utiltrace.Field{"err", err})
@@ -467,8 +484,8 @@ func (s *store) GuaranteedUpdate(
467484
trace.Step("Transaction committed")
468485
if !txnResp.Succeeded {
469486
getResp := (*clientv3.GetResponse)(txnResp.Responses[0].GetResponseRange())
470-
klog.V(4).Infof("GuaranteedUpdate of %s failed because of a conflict, going to retry", key)
471-
origState, err = s.getState(ctx, getResp, key, v, ignoreNotFound)
487+
klog.V(4).Infof("GuaranteedUpdate of %s failed because of a conflict, going to retry", preparedKey)
488+
origState, err = s.getState(ctx, getResp, preparedKey, v, ignoreNotFound)
472489
if err != nil {
473490
return err
474491
}
@@ -502,18 +519,21 @@ func getNewItemFunc(listObj runtime.Object, v reflect.Value) func() runtime.Obje
502519
}
503520

504521
func (s *store) Count(key string) (int64, error) {
505-
key = path.Join(s.pathPrefix, key)
522+
preparedKey, err := s.prepareKey(key)
523+
if err != nil {
524+
return 0, err
525+
}
506526

507527
// We need to make sure the key ended with "/" so that we only get children "directories".
508528
// e.g. if we have key "/a", "/a/b", "/ab", getting keys with prefix "/a" will return all three,
509529
// while with prefix "/a/" will return only "/a/b" which is the correct answer.
510-
if !strings.HasSuffix(key, "/") {
511-
key += "/"
530+
if !strings.HasSuffix(preparedKey, "/") {
531+
preparedKey += "/"
512532
}
513533

514534
startTime := time.Now()
515-
getResp, err := s.client.KV.Get(context.Background(), key, clientv3.WithRange(clientv3.GetPrefixRangeEnd(key)), clientv3.WithCountOnly())
516-
metrics.RecordEtcdRequestLatency("listWithCount", key, startTime)
535+
getResp, err := s.client.KV.Get(context.Background(), preparedKey, clientv3.WithRange(clientv3.GetPrefixRangeEnd(preparedKey)), clientv3.WithCountOnly())
536+
metrics.RecordEtcdRequestLatency("listWithCount", preparedKey, startTime)
517537
if err != nil {
518538
return 0, err
519539
}
@@ -522,6 +542,10 @@ func (s *store) Count(key string) (int64, error) {
522542

523543
// GetList implements storage.Interface.
524544
func (s *store) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error {
545+
preparedKey, err := s.prepareKey(key)
546+
if err != nil {
547+
return err
548+
}
525549
recursive := opts.Recursive
526550
resourceVersion := opts.ResourceVersion
527551
match := opts.ResourceVersionMatch
@@ -542,16 +566,15 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
542566
if err != nil || v.Kind() != reflect.Slice {
543567
return fmt.Errorf("need ptr to slice: %v", err)
544568
}
545-
key = path.Join(s.pathPrefix, key)
546569

547570
// For recursive lists, we need to make sure the key ended with "/" so that we only
548571
// get children "directories". e.g. if we have key "/a", "/a/b", "/ab", getting keys
549572
// with prefix "/a" will return all three, while with prefix "/a/" will return only
550573
// "/a/b" which is the correct answer.
551-
if recursive && !strings.HasSuffix(key, "/") {
552-
key += "/"
574+
if recursive && !strings.HasSuffix(preparedKey, "/") {
575+
preparedKey += "/"
553576
}
554-
keyPrefix := key
577+
keyPrefix := preparedKey
555578

556579
// set the appropriate clientv3 options to filter the returned data set
557580
var limitOption *clientv3.OpOption
@@ -590,7 +613,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
590613

591614
rangeEnd := clientv3.GetPrefixRangeEnd(keyPrefix)
592615
options = append(options, clientv3.WithRange(rangeEnd))
593-
key = continueKey
616+
preparedKey = continueKey
594617

595618
// If continueRV > 0, the LIST request needs a specific resource version.
596619
// continueRV==0 is invalid.
@@ -657,7 +680,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
657680
}()
658681
for {
659682
startTime := time.Now()
660-
getResp, err = s.client.KV.Get(ctx, key, options...)
683+
getResp, err = s.client.KV.Get(ctx, preparedKey, options...)
661684
if recursive {
662685
metrics.RecordEtcdRequestLatency("list", getTypeName(listPtr), startTime)
663686
} else {
@@ -729,7 +752,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
729752
}
730753
*limitOption = clientv3.WithLimit(limit)
731754
}
732-
key = string(lastKey) + "\x00"
755+
preparedKey = string(lastKey) + "\x00"
733756
if withRev == 0 {
734757
withRev = returnedRV
735758
options = append(options, clientv3.WithRev(withRev))
@@ -794,12 +817,15 @@ func growSlice(v reflect.Value, maxCapacity int, sizes ...int) {
794817

795818
// Watch implements storage.Interface.Watch.
796819
func (s *store) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) {
820+
preparedKey, err := s.prepareKey(key)
821+
if err != nil {
822+
return nil, err
823+
}
797824
rev, err := s.versioner.ParseResourceVersion(opts.ResourceVersion)
798825
if err != nil {
799826
return nil, err
800827
}
801-
key = path.Join(s.pathPrefix, key)
802-
return s.watcher.Watch(ctx, key, int64(rev), opts.Recursive, opts.ProgressNotify, opts.Predicate)
828+
return s.watcher.Watch(ctx, preparedKey, int64(rev), opts.Recursive, opts.ProgressNotify, opts.Predicate)
803829
}
804830

805831
func (s *store) getState(ctx context.Context, getResp *clientv3.GetResponse, key string, v reflect.Value, ignoreNotFound bool) (*objState, error) {
@@ -911,6 +937,30 @@ func (s *store) validateMinimumResourceVersion(minimumResourceVersion string, ac
911937
return nil
912938
}
913939

940+
func (s *store) prepareKey(key string) (string, error) {
941+
if key == ".." ||
942+
strings.HasPrefix(key, "../") ||
943+
strings.HasSuffix(key, "/..") ||
944+
strings.Contains(key, "/../") {
945+
return "", fmt.Errorf("invalid key: %q", key)
946+
}
947+
if key == "." ||
948+
strings.HasPrefix(key, "./") ||
949+
strings.HasSuffix(key, "/.") ||
950+
strings.Contains(key, "/./") {
951+
return "", fmt.Errorf("invalid key: %q", key)
952+
}
953+
if key == "" || key == "/" {
954+
return "", fmt.Errorf("empty key: %q", key)
955+
}
956+
// We ensured that pathPrefix ends in '/' in construction, so skip any leading '/' in the key now.
957+
startIndex := 0
958+
if key[0] == '/' {
959+
startIndex = 1
960+
}
961+
return s.pathPrefix + key[startIndex:], nil
962+
}
963+
914964
// decode decodes value of bytes into object. It will also set the object resource version to rev.
915965
// On success, objPtr would be set to the object.
916966
func decode(codec runtime.Codec, versioner storage.Versioner, value []byte, objPtr runtime.Object, rev int64) error {

0 commit comments

Comments
 (0)