fieldpath's Paved.SetValue allows growing arrays up to arbitrary sizes in crossplane-runtime
Description
crossplane-runtime is a set of go libraries used to build Kubernetes controllers in Crossplane and its related stacks. An out of memory panic vulnerability has been discovered in affected versions. Applications that use the Paved type's SetValue method with user provided input without proper validation might use excessive amounts of memory and cause an out of memory panic. In the fieldpath package, the Paved.SetValue method sets a value on the Paved object according to the provided path, without any validation. This allows setting values in slices at any provided index, which grows the target array up to the requested index, the index is currently capped at max uint32 (4294967295) given how indexes are parsed, but that is still an unnecessarily large value. If callers are not validating paths' indexes on their own, which most probably are not going to do, given that the input is parsed directly in the SetValue method, this could allow users to consume arbitrary amounts of memory. Applications that do not use the Paved type's SetValue method are not affected. This issue has been addressed in versions 0.16.1 and 0.19.2. Users are advised to upgrade. Users unable to upgrade can parse and validate the path before passing it to the SetValue method of the Paved type, constraining the index size as deemed appropriate.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
crossplane-runtime's Paved.SetValue method lacks validation on slice indices, allowing an attacker to cause an out-of-memory panic via a path with a large index.
Vulnerability
Overview
crossplane-runtime provides the Paved type's SetValue method to set values in Kubernetes objects via field paths. The method accepts a path string that may include slice indices, but it does not validate the index value before allocating memory. This allows an attacker to supply a path with an index as large as max uint32 (4294967295), causing the target slice to be grown to that index, consuming excessive memory and resulting in an out-of-memory panic [1][4].
Attack
Vector
An attacker who can control the field path passed to SetValue (e.g., through user input) can exploit this vulnerability without prior authentication. The path is parsed directly in SetValue, so if applications do not validate indexes themselves, they are vulnerable [1].
Impact
A successful exploit causes the Go runtime to panic due to out-of-memory, effectively leading to a denial of service (DoS) condition on the affected application. Applications that do not use the Paved type's SetValue method are not affected [4].
Mitigation
The issue is fixed in crossplane-runtime versions 0.16.1 and 0.19.2. The fix introduces a configurable maxFieldPathIndex (default: 1024) and a WithMaxFieldPathIndex option to set a limit, preventing unbounded slice growth [3]. Users unable to upgrade should parse and validate the path before passing it to SetValue, constraining the index size as appropriate [4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/crossplane/crossplane-runtimeGo | >= 0.17.0, < 0.19.2 | 0.19.2 |
github.com/crossplane/crossplane-runtimeGo | >= 0.6.0, < 0.16.1 | 0.16.1 |
Affected products
3- Range: <0.16.1, <0.19.2
- crossplane/crossplane-runtimev5Range: >= 0.17.0, < 0.19.2
Patches
253508a9f4374Merge pull request from GHSA-vfvj-3m3g-m532
2 files changed · +67 −6
pkg/fieldpath/paved.go+32 −5 modified@@ -25,6 +25,9 @@ import ( "github.com/crossplane/crossplane-runtime/pkg/errors" ) +// DefaultMaxFieldPathIndex is the max allowed index in a field path. +const DefaultMaxFieldPathIndex = 1024 + type errNotFound struct { error } @@ -46,19 +49,39 @@ func IsNotFound(err error) bool { // A Paved JSON object supports getting and setting values by their field path. type Paved struct { - object map[string]any + object map[string]any + maxFieldPathIndex uint } +type PavedOption func(paved *Paved) + // PaveObject paves a runtime.Object, making it possible to get and set values // by field path. o must be a non-nil pointer to an object. -func PaveObject(o runtime.Object) (*Paved, error) { +func PaveObject(o runtime.Object, opts ...PavedOption) (*Paved, error) { u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) - return Pave(u), errors.Wrap(err, "cannot convert object to unstructured data") + return Pave(u, opts...), errors.Wrap(err, "cannot convert object to unstructured data") } // Pave a JSON object, making it possible to get and set values by field path. -func Pave(object map[string]any) *Paved { - return &Paved{object: object} +func Pave(object map[string]any, opts ...PavedOption) *Paved { + p := &Paved{object: object, maxFieldPathIndex: DefaultMaxFieldPathIndex} + + for _, opt := range opts { + opt(p) + } + + return p +} + +// WithMaxFieldPathIndex returns a PavedOption that sets the max allowed index for field paths, 0 means no limit. +func WithMaxFieldPathIndex(max uint) PavedOption { + return func(paved *Paved) { + paved.maxFieldPathIndex = max + } +} + +func (p *Paved) maxFieldPathIndexEnabled() bool { + return p.maxFieldPathIndex > 0 } // MarshalJSON to the underlying object. @@ -358,6 +381,10 @@ func (p *Paved) setValue(s Segments, value any) error { return errors.Errorf("%s is not an array", s[:i]) } + if p.maxFieldPathIndexEnabled() && current.Index > p.maxFieldPathIndex { + return errors.Errorf("index %d is greater than max allowed index %d", current.Index, p.maxFieldPathIndex) + } + if final { array[current.Index] = v return nil
pkg/fieldpath/paved_test.go+35 −1 modified@@ -17,6 +17,7 @@ limitations under the License. package fieldpath import ( + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -593,6 +594,7 @@ func TestSetValue(t *testing.T) { type args struct { path string value any + opts []PavedOption } type want struct { object map[string]any @@ -737,6 +739,38 @@ func TestSetValue(t *testing.T) { }, }, }, + "RejectsHighIndexes": { + reason: "Paths having indexes above the maximum default value are rejected", + data: []byte(`{"data":["a"]}`), + args: args{ + path: fmt.Sprintf("data[%v]", MaxFieldPathIndex+1), + value: "c", + }, + want: want{ + object: map[string]any{ + "data": []any{"a"}}, + err: errors.Wrap(errors.Errorf("found index above max (%[1]v > %[2]v): data[%[1]v]", + MaxFieldPathIndex+1, MaxFieldPathIndex), "invalid segments"), + }, + }, + "NotRejectsHighIndexesIfNoDefaultOptions": { + reason: "Paths having indexes above the maximum default value are not rejected if default disabled", + data: []byte(`{"data":["a"]}`), + args: args{ + path: fmt.Sprintf("data[%v]", MaxFieldPathIndex+1), + value: "c", + opts: []PavedOption{}, + }, + want: want{ + object: map[string]any{ + "data": func() []any { + res := make([]any, MaxFieldPathIndex+2) + res[0] = "a" + res[MaxFieldPathIndex+1] = "c" + return res + }()}, + }, + }, "MapStringString": { reason: "A map of string to string should be converted to a map of string to any", data: []byte(`{"metadata":{}}`), @@ -817,7 +851,7 @@ func TestSetValue(t *testing.T) { t.Run(name, func(t *testing.T) { in := make(map[string]any) _ = json.Unmarshal(tc.data, &in) - p := Pave(in) + p := Pave(in, tc.args.opts...) err := p.SetValue(tc.args.path, tc.args.value) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
f67177024d90Add a package for getting and setting unstructured fields by path
4 files changed · +1486 −0
pkg/fieldpath/fieldpath.go+280 −0 added@@ -0,0 +1,280 @@ +/* +Copyright 2019 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package fieldpath provides utilities for working with field paths. +// +// Field paths reference a field within a Kubernetes object via a simple string. +// API conventions describe the syntax as "standard JavaScript syntax for +// accessing that field, assuming the JSON object was transformed into a +// JavaScript object, without the leading dot, such as metadata.name". +// +// Valid examples: +// +// * metadata.name +// * spec.containers[0].name +// * data[.config.yml] +// * metadata.annotations['crossplane.io/external-name'] +// * spec.items[0][8] +// * apiVersion +// * [42] +// +// Invalid examples: +// +// * .metadata.name - Leading period. +// * metadata..name - Double period. +// * metadata.name. - Trailing period. +// * spec.containers[] - Empty brackets. +// * spec.containers.[0].name - Period before open bracket. +// +// https://github.com/kubernetes/community/blob/61f3d0/contributors/devel/sig-architecture/api-conventions.md#selecting-fields +package fieldpath + +import ( + "fmt" + "strconv" + "strings" + "unicode/utf8" + + "github.com/pkg/errors" +) + +// A SegmentType within a field path; either a field within an object, or an +// index within an array. +type SegmentType int + +// Segment types. +const ( + _ SegmentType = iota + SegmentField + SegmentIndex +) + +// A Segment of a field path. +type Segment struct { + Type SegmentType + Field string + Index uint +} + +// Segments of a field path. +type Segments []Segment + +func (sg Segments) String() string { + var b strings.Builder + + for _, s := range sg { + switch s.Type { + case SegmentField: + if strings.ContainsRune(s.Field, period) { + b.WriteString(fmt.Sprintf("[%s]", s.Field)) + continue + } + b.WriteString(fmt.Sprintf(".%s", s.Field)) + case SegmentIndex: + b.WriteString(fmt.Sprintf("[%d]", s.Index)) + } + } + + return strings.TrimPrefix(b.String(), ".") +} + +// FieldOrIndex produces a new segment from the supplied string. The segment is +// considered to be an array index if the string can be interpreted as an +// unsigned 32 bit integer. Anything else is interpreted as an object field +// name. +func FieldOrIndex(s string) Segment { + // Attempt to parse the segment as an unsigned integer. If the integer is + // larger than 2^32 (the limit for most JSON arrays) we presume it's too big + // to be an array index, and is thus a field name. + if i, err := strconv.ParseUint(s, 10, 32); err == nil { + return Segment{Type: SegmentIndex, Index: uint(i)} + } + + // If the segment is not a valid unsigned integer we presume it's + // a string field name. + return Field(s) +} + +// Field produces a new segment from the supplied string. The segment is always +// considered to be an object field name. +func Field(s string) Segment { + return Segment{Type: SegmentField, Field: strings.Trim(s, "'\"")} +} + +// Parse the supplied path into a slice of Segments. +func Parse(path string) (Segments, error) { + l := &lexer{input: path, items: make(chan item)} + go l.run() + + segments := make(Segments, 0, 1) + for i := range l.items { + switch i.typ { + case itemField: + segments = append(segments, Field(i.val)) + case itemFieldOrIndex: + segments = append(segments, FieldOrIndex(i.val)) + case itemError: + return nil, errors.Errorf("%s at position %d", i.val, i.pos) + } + } + return segments, nil +} + +const ( + period = '.' + leftBracket = '[' + rightBracket = ']' +) + +type itemType int + +const ( + itemError itemType = iota + itemPeriod + itemLeftBracket + itemRightBracket + itemField + itemFieldOrIndex + itemEOL +) + +type item struct { + typ itemType + pos int + val string +} + +type stateFn func(*lexer) stateFn + +// A simplified version of the text/template lexer. +// https://github.com/golang/go/blob/6396bc9d/src/text/template/parse/lex.go#L108 +type lexer struct { + input string + pos int + start int + items chan item +} + +func (l *lexer) run() { + for state := lexField; state != nil; { + state = state(l) + } + close(l.items) +} + +func (l *lexer) emit(t itemType) { + // Don't emit empty values. + if l.pos <= l.start { + return + } + l.items <- item{typ: t, pos: l.start, val: l.input[l.start:l.pos]} + l.start = l.pos +} + +func (l *lexer) errorf(pos int, format string, args ...interface{}) stateFn { + l.items <- item{typ: itemError, pos: pos, val: fmt.Sprintf(format, args...)} + return nil +} + +func lexField(l *lexer) stateFn { + for i, r := range l.input[l.pos:] { + switch r { + // A right bracket may not appear in an object field name. + case rightBracket: + return l.errorf(l.pos+i, "unexpected %q", rightBracket) + + // A left bracket indicates the end of the field name. + case leftBracket: + l.pos += i + l.emit(itemField) + return lexLeftBracket + + // A period indicates the end of the field name. + case period: + l.pos += i + l.emit(itemField) + return lexPeriod + } + } + + // The end of the input indicates the end of the field name. + l.pos = len(l.input) + l.emit(itemField) + l.emit(itemEOL) + return nil +} + +func lexPeriod(l *lexer) stateFn { + // A period may not appear at the beginning or the end of the input. + if l.pos == 0 || l.pos == len(l.input)-1 { + return l.errorf(l.pos, "unexpected %q", period) + } + + l.pos += utf8.RuneLen(period) + l.emit(itemPeriod) + + // A period may only be followed by a field name. We defer checking for + // right brackets to lexField, where they are invalid. + r, _ := utf8.DecodeRuneInString(l.input[l.pos:]) + if r == period { + return l.errorf(l.pos, "unexpected %q", period) + } + if r == leftBracket { + return l.errorf(l.pos, "unexpected %q", leftBracket) + } + + return lexField +} + +func lexLeftBracket(l *lexer) stateFn { + // A right bracket must appear before the input ends. + if !strings.ContainsRune(l.input[l.pos:], rightBracket) { + return l.errorf(l.pos, "unterminated %q", leftBracket) + } + + l.pos += utf8.RuneLen(leftBracket) + l.emit(itemLeftBracket) + return lexFieldOrIndex +} + +// Strings between brackets may be either a field name or an array index. +// Periods have no special meaning in this context. +func lexFieldOrIndex(l *lexer) stateFn { + // We know a right bracket exists before EOL thanks to the preceding + // lexLeftBracket. + rbi := strings.IndexRune(l.input[l.pos:], rightBracket) + + // A right bracket may not immediately follow a left bracket. + if rbi == 0 { + return l.errorf(l.pos, "unexpected %q", rightBracket) + } + + // A left bracket may not appear before the next right bracket. + if lbi := strings.IndexRune(l.input[l.pos:l.pos+rbi], leftBracket); lbi > -1 { + return l.errorf(l.pos+lbi, "unexpected %q", leftBracket) + } + + // Periods are not considered field separators when we're inside brackets. + l.pos += rbi + l.emit(itemFieldOrIndex) + return lexRightBracket +} + +func lexRightBracket(l *lexer) stateFn { + l.pos += utf8.RuneLen(rightBracket) + l.emit(itemRightBracket) + return lexField +}
pkg/fieldpath/fieldpath_test.go+283 −0 added@@ -0,0 +1,283 @@ +/* +Copyright 2019 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fieldpath + +import ( + "math" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +func TestSegments(t *testing.T) { + cases := map[string]struct { + s Segments + want string + }{ + "SingleField": { + s: Segments{Field("spec")}, + want: "spec", + }, + "SingleIndex": { + s: Segments{FieldOrIndex("0")}, + want: "[0]", + }, + "FieldsAndIndex": { + s: Segments{ + Field("spec"), + Field("containers"), + FieldOrIndex("0"), + Field("name"), + }, + want: "spec.containers[0].name", + }, + "PeriodsInField": { + s: Segments{ + Field("data"), + Field(".config.yml"), + }, + want: "data[.config.yml]", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + if diff := cmp.Diff(tc.want, tc.s.String()); diff != "" { + t.Errorf("s.String(): -want, +got:\n %s", diff) + } + + }) + } +} + +func TestFieldOrIndex(t *testing.T) { + cases := map[string]struct { + reason string + s string + want Segment + }{ + "Field": { + reason: "An unambiguous string should be interpreted as a field segment", + s: "coolField", + want: Segment{Type: SegmentField, Field: "coolField"}, + }, + "QuotedField": { + reason: "A quoted string should be interpreted as a field segment with the quotes removed", + s: "'coolField'", + want: Segment{Type: SegmentField, Field: "coolField"}, + }, + "Index": { + reason: "An unambiguous integer should be interpreted as an index segment", + s: "3", + want: Segment{Type: SegmentIndex, Index: 3}, + }, + "Negative": { + reason: "A negative integer should be interpreted as an field segment", + s: "-3", + want: Segment{Type: SegmentField, Field: "-3"}, + }, + "Float": { + reason: "A float should be interpreted as an field segment", + s: "3.0", + want: Segment{Type: SegmentField, Field: "3.0"}, + }, + "Overflow": { + reason: "A very big integer will be interpreted as a field segment", + s: strconv.Itoa(math.MaxUint32 + 1), + want: Segment{Type: SegmentField, Field: strconv.Itoa(math.MaxUint32 + 1)}, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := FieldOrIndex(tc.s) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("\nFieldOrIndex(...): %s: -want, +got:\n%s", tc.reason, diff) + } + }) + } +} + +func TestParse(t *testing.T) { + type want struct { + s Segments + err error + } + + cases := map[string]struct { + reason string + path string + want want + }{ + "SingleField": { + reason: "A path with no brackets or periods should be interpreted as a single field segment", + path: "spec", + want: want{ + s: Segments{Field("spec")}, + }, + }, + "SingleIndex": { + reason: "An integer surrounded by brackets should be interpreted as an index", + path: "[0]", + want: want{ + s: Segments{FieldOrIndex("0")}, + }, + }, + "TwoFields": { + reason: "A path with one period should be interpreted as two field segments", + path: "metadata.name", + want: want{ + s: Segments{Field("metadata"), Field("name")}, + }, + }, + "APIConventionsExample": { + reason: "The example given by the Kubernetes API convention should be parse correctly", + path: "fields[1].state.current", + want: want{ + s: Segments{ + Field("fields"), + FieldOrIndex("1"), + Field("state"), + Field("current"), + }, + }, + }, + "SimpleIndex": { + reason: "Indexing an object field that is an array should result in a field and an index", + path: "items[0]", + want: want{ + s: Segments{Field("items"), FieldOrIndex("0")}, + }, + }, + "FieldsAndIndex": { + reason: "A path with periods and braces should be interpreted as fields and indices", + path: "spec.containers[0].name", + want: want{ + s: Segments{ + Field("spec"), + Field("containers"), + FieldOrIndex("0"), + Field("name"), + }, + }, + }, + "NestedArray": { + reason: "A nested array should result in two consecutive index fields", + path: "nested[0][1].name", + want: want{ + s: Segments{ + Field("nested"), + FieldOrIndex("0"), + FieldOrIndex("1"), + Field("name"), + }, + }, + }, + "BracketStyleField": { + reason: "A field name can be specified using brackets rather than a period", + path: "spec[containers][0].name", + want: want{ + s: Segments{ + Field("spec"), + Field("containers"), + FieldOrIndex("0"), + Field("name"), + }, + }, + }, + "BracketFieldWithPeriod": { + reason: "A field name specified using brackets can include a period", + path: "data[.config.yml]", + want: want{ + s: Segments{ + Field("data"), + FieldOrIndex(".config.yml"), + }, + }, + }, + "LeadingPeriod": { + reason: "A path may not start with a period (unlike a JSON path)", + path: ".metadata.name", + want: want{ + err: errors.New("unexpected '.' at position 0"), + }, + }, + "TrailingPeriod": { + reason: "A path may not end with a period", + path: "metadata.name.", + want: want{ + err: errors.New("unexpected '.' at position 13"), + }, + }, + "BracketsFollowingPeriod": { + reason: "Brackets may not follow a period", + path: "spec.containers.[0].name", + want: want{ + err: errors.New("unexpected '[' at position 16"), + }, + }, + "DoublePeriod": { + reason: "A path may not include two consecutive periods", + path: "metadata..name", + want: want{ + err: errors.New("unexpected '.' at position 9"), + }, + }, + "DanglingRightBracket": { + reason: "A right bracket may not appear in a field name", + path: "metadata.]name", + want: want{ + err: errors.New("unexpected ']' at position 9"), + }, + }, + "DoubleOpenBracket": { + reason: "Brackets may not be nested", + path: "spec[bracketed[name]]", + want: want{ + err: errors.New("unexpected '[' at position 14"), + }, + }, + "DanglingLeftBracket": { + reason: "A left bracket must be closed", + path: "spec[name", + want: want{ + err: errors.New("unterminated '[' at position 4"), + }, + }, + "EmptyBracket": { + reason: "Brackets may not be empty", + path: "spec[]", + want: want{ + err: errors.New("unexpected ']' at position 5"), + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got, err := Parse(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\nParse(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.s, got); diff != "" { + t.Errorf("\nParse(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +}
pkg/fieldpath/paved.go+307 −0 added@@ -0,0 +1,307 @@ +/* +Copyright 2019 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fieldpath + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// A Paved JSON object supports getting and setting values by their field path. +type Paved struct { + object map[string]interface{} +} + +// Pave a JSON object, making it possible to get and set values by field path. +func Pave(object map[string]interface{}) *Paved { + return &Paved{object: object} +} + +// MarshalJSON to the underlying object. +func (p Paved) MarshalJSON() ([]byte, error) { + return json.Marshal(p.object) +} + +// UnmarshalJSON from the underlying object. +func (p *Paved) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &p.object) +} + +func (p *Paved) getValue(s Segments) (interface{}, error) { + var it interface{} = p.object + for i, current := range s { + final := i == len(s)-1 + switch current.Type { + case SegmentIndex: + array, ok := it.([]interface{}) + if !ok { + return nil, errors.Errorf("%s: not an array", s[:i]) + } + if int(current.Index) >= len(array) { + return nil, errors.Errorf("%s: no such element", s[:i+1]) + } + if final { + return array[current.Index], nil + } + it = array[current.Index] + + case SegmentField: + object, ok := it.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("%s: not an object", s[:i]) + } + v, ok := object[current.Field] + if !ok { + return nil, errors.Errorf("%s: no such field", s[:i+1]) + } + if final { + return v, nil + } + it = object[current.Field] + } + } + + // This should be unreachable. + return nil, nil +} + +// GetValue of the supplied field path. +func (p *Paved) GetValue(path string) (interface{}, error) { + segments, err := Parse(path) + if err != nil { + return nil, errors.Wrapf(err, "cannot parse path %q", path) + } + + return p.getValue(segments) +} + +// GetString value of the supplied field path. +func (p *Paved) GetString(path string) (string, error) { + v, err := p.GetValue(path) + if err != nil { + return "", err + } + + s, ok := v.(string) + if !ok { + return "", errors.Errorf("%s: not a string", path) + } + return s, nil +} + +// GetStringArray value of the supplied field path. +func (p *Paved) GetStringArray(path string) ([]string, error) { + v, err := p.GetValue(path) + if err != nil { + return nil, err + } + + a, ok := v.([]interface{}) + if !ok { + return nil, errors.Errorf("%s: not an array", path) + } + + sa := make([]string, len(a)) + for i := range a { + s, ok := a[i].(string) + if !ok { + return nil, errors.Errorf("%s: not an array of strings", path) + } + sa[i] = s + } + + return sa, nil +} + +// GetStringObject value of the supplied field path. +func (p *Paved) GetStringObject(path string) (map[string]string, error) { + v, err := p.GetValue(path) + if err != nil { + return nil, err + } + + o, ok := v.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("%s: not an object", path) + } + + so := make(map[string]string) + for k, in := range o { + s, ok := in.(string) + if !ok { + return nil, errors.Errorf("%s: not an object with string field values", path) + } + so[k] = s + + } + + return so, nil +} + +// GetBool value of the supplied field path. +func (p *Paved) GetBool(path string) (bool, error) { + v, err := p.GetValue(path) + if err != nil { + return false, err + } + + b, ok := v.(bool) + if !ok { + return false, errors.Errorf("%s: not a bool", path) + } + return b, nil +} + +// GetNumber value of the supplied field path. +func (p *Paved) GetNumber(path string) (float64, error) { + v, err := p.GetValue(path) + if err != nil { + return 0, err + } + + f, ok := v.(float64) + if !ok { + return 0, errors.Errorf("%s: not a (float64) number", path) + } + return f, nil +} + +func (p *Paved) setValue(s Segments, value interface{}) error { + var in interface{} = p.object + for i, current := range s { + final := i == len(s)-1 + + switch current.Type { + case SegmentIndex: + array, ok := in.([]interface{}) + if !ok { + return errors.Errorf("%s is not an array", s[:i]) + } + + if final { + array[current.Index] = value + return nil + } + + prepareElement(array, current, s[i+1]) + in = array[current.Index] + + case SegmentField: + object, ok := in.(map[string]interface{}) + if !ok { + return errors.Errorf("%s is not an object", s[:i]) + } + + if final { + object[current.Field] = value + return nil + } + + prepareField(object, current, s[i+1]) + in = object[current.Field] + } + } + + return nil +} + +func prepareElement(array []interface{}, current, next Segment) { + // If this segment is not the final one and doesn't exist we need to + // create it for our next segment. + if array[current.Index] == nil { + switch next.Type { + case SegmentIndex: + array[current.Index] = make([]interface{}, next.Index+1) + case SegmentField: + array[current.Index] = make(map[string]interface{}) + } + return + } + + // If our next segment indexes an array that exists in our current segment's + // element we must ensure the array is long enough to set the next segment. + if next.Type != SegmentIndex { + return + } + + na, ok := array[current.Index].([]interface{}) + if !ok { + return + } + + if int(next.Index) < len(na) { + return + } + + array[current.Index] = append(na, make([]interface{}, int(next.Index)-len(na)+1)...) +} + +func prepareField(object map[string]interface{}, current, next Segment) { + // If this segment is not the final one and doesn't exist we need to + // create it for our next segment. + if _, ok := object[current.Field]; !ok { + switch next.Type { + case SegmentIndex: + object[current.Field] = make([]interface{}, next.Index+1) + case SegmentField: + object[current.Field] = make(map[string]interface{}) + } + return + } + + // If our next segment indexes an array that exists in our current segment's + // field we must ensure the array is long enough to set the next segment. + if next.Type != SegmentIndex { + return + } + + na, ok := object[current.Field].([]interface{}) + if !ok { + return + } + + if int(next.Index) < len(na) { + return + } + + object[current.Field] = append(na, make([]interface{}, int(next.Index)-len(na)+1)...) +} + +// SetValue at the supplied field path. +func (p *Paved) SetValue(path string, value interface{}) error { + segments, err := Parse(path) + if err != nil { + return errors.Wrapf(err, "cannot parse path %q", path) + } + return p.setValue(segments, value) +} + +// SetString value at the supplied field path. +func (p *Paved) SetString(path, value string) error { + return p.SetValue(path, value) +} + +// SetBool value at the supplied field path. +func (p *Paved) SetBool(path string, value bool) error { + return p.SetValue(path, value) +} + +// SetNumber value at the supplied field path. +func (p *Paved) SetNumber(path string, value float64) error { + return p.SetValue(path, value) +}
pkg/fieldpath/paved_test.go+616 −0 added@@ -0,0 +1,616 @@ +/* +Copyright 2019 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fieldpath + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/pkg/errors" + + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +func TestGetValue(t *testing.T) { + type want struct { + value interface{} + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "MetadataName": { + reason: "It should be possible to get a field from a nested object", + path: "metadata.name", + data: []byte(`{"metadata":{"name":"cool"}}`), + want: want{ + value: "cool", + }, + }, + "ContainerName": { + reason: "It should be possible to get a field from an object array element", + path: "spec.containers[0].name", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + want: want{ + value: "cool", + }, + }, + "NestedArray": { + reason: "It should be possible to get a field from a nested array", + path: "items[0][1]", + data: []byte(`{"items":[["a", "b"]]}`), + want: want{ + value: "b", + }, + }, + "OwnerRefController": { + reason: "Requesting a boolean field path should work.", + path: "metadata.ownerRefs[0].controller", + data: []byte(`{"metadata":{"ownerRefs":[{"controller": true}]}}`), + want: want{ + value: true, + }, + }, + "MetadataVersion": { + reason: "Requesting a number field should work", + path: "metadata.version", + data: []byte(`{"metadata":{"version":2}}`), + want: want{ + value: float64(2), + }, + }, + "MetadataNope": { + reason: "Requesting a non-existent object field should fail", + path: "metadata.name", + data: []byte(`{"metadata":{"nope":"cool"}}`), + want: want{ + err: errors.New("metadata.name: no such field"), + }, + }, + "InsufficientContainers": { + reason: "Requesting a non-existent array element should fail", + path: "spec.containers[1].name", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + want: want{ + err: errors.New("spec.containers[1]: no such element"), + }, + }, + "NotAnArray": { + reason: "Indexing an object should fail", + path: "metadata[1]", + data: []byte(`{"metadata":{"nope":"cool"}}`), + want: want{ + err: errors.New("metadata: not an array"), + }, + }, + "NotAnObject": { + reason: "Requesting a field in an array should fail", + path: "spec.containers[nope].name", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + want: want{ + err: errors.New("spec.containers: not an object"), + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.GetValue(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.GetValue(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.value, got); diff != "" { + t.Errorf("\np.GetValue(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} + +func TestGetString(t *testing.T) { + type want struct { + value string + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "MetadataName": { + reason: "It should be possible to get a field from a nested object", + path: "metadata.name", + data: []byte(`{"metadata":{"name":"cool"}}`), + want: want{ + value: "cool", + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + "NotAString": { + reason: "Requesting an non-string field path should fail", + path: "metadata.version", + data: []byte(`{"metadata":{"version":2}}`), + want: want{ + err: errors.New("metadata.version: not a string"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.GetString(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.GetString(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.value, got); diff != "" { + t.Errorf("\np.GetString(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} + +func TestGetStringArray(t *testing.T) { + type want struct { + value []string + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "MetadataLabels": { + reason: "It should be possible to get a field from a nested object", + path: "spec.containers[0].command", + data: []byte(`{"spec": {"containers": [{"command": ["/bin/bash"]}]}}`), + want: want{ + value: []string{"/bin/bash"}, + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + "NotAnArray": { + reason: "Requesting an non-object field path should fail", + path: "metadata.version", + data: []byte(`{"metadata":{"version":2}}`), + want: want{ + err: errors.New("metadata.version: not an array"), + }, + }, + "NotAStringArray": { + reason: "Requesting an non-string-object field path should fail", + path: "metadata.versions", + data: []byte(`{"metadata":{"versions":[1,2]}}`), + want: want{ + err: errors.New("metadata.versions: not an array of strings"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.GetStringArray(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.GetStringArray(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.value, got); diff != "" { + t.Errorf("\np.GetStringArray(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} + +func TestGetStringObject(t *testing.T) { + type want struct { + value map[string]string + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "MetadataLabels": { + reason: "It should be possible to get a field from a nested object", + path: "metadata.labels", + data: []byte(`{"metadata":{"labels":{"cool":"true"}}}`), + want: want{ + value: map[string]string{"cool": "true"}, + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + "NotAnObject": { + reason: "Requesting an non-object field path should fail", + path: "metadata.version", + data: []byte(`{"metadata":{"version":2}}`), + want: want{ + err: errors.New("metadata.version: not an object"), + }, + }, + "NotAStringObject": { + reason: "Requesting an non-string-object field path should fail", + path: "metadata.versions", + data: []byte(`{"metadata":{"versions":{"a": 2}}}`), + want: want{ + err: errors.New("metadata.versions: not an object with string field values"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.GetStringObject(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.GetStringObject(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.value, got); diff != "" { + t.Errorf("\np.GetStringObject(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} + +func TestGetBool(t *testing.T) { + type want struct { + value bool + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "OwnerRefController": { + reason: "Requesting a boolean field path should work.", + path: "metadata.ownerRefs[0].controller", + data: []byte(`{"metadata":{"ownerRefs":[{"controller": true}]}}`), + want: want{ + value: true, + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + "NotABool": { + reason: "Requesting an non-boolean field path should fail", + path: "metadata.name", + data: []byte(`{"metadata":{"name":"cool"}}`), + want: want{ + err: errors.New("metadata.name: not a bool"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.GetBool(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.GetBool(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.value, got); diff != "" { + t.Errorf("\np.GetBool(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} + +func TestGetNumber(t *testing.T) { + type want struct { + value float64 + err error + } + cases := map[string]struct { + reason string + path string + data []byte + want want + }{ + "MetadataVersion": { + reason: "Requesting a number field should work", + path: "metadata.version", + data: []byte(`{"metadata":{"version":2}}`), + want: want{ + value: 2, + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + path: "spec[]", + want: want{ + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + "NotANumber": { + reason: "Requesting an non-number field path should fail", + path: "metadata.name", + data: []byte(`{"metadata":{"name":"cool"}}`), + want: want{ + err: errors.New("metadata.name: not a (float64) number"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + got, err := p.GetNumber(tc.path) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.GetNumber(%s): %s: -want error, +got error:\n%s", tc.path, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.value, got); diff != "" { + t.Errorf("\np.GetNumber(%s): %s: -want, +got:\n%s", tc.path, tc.reason, diff) + } + }) + } +} + +func TestSetValue(t *testing.T) { + type args struct { + path string + value interface{} + } + type want struct { + object map[string]interface{} + err error + } + cases := map[string]struct { + reason string + data []byte + args args + want want + }{ + "MetadataName": { + reason: "Setting an object field should work", + data: []byte(`{"metadata":{"name":"lame"}}`), + args: args{ + path: "metadata.name", + value: "cool", + }, + want: want{ + object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "cool", + }, + }, + }, + }, + "NonExistentMetadataName": { + reason: "Setting a non-existent object field should work", + data: []byte(`{}`), + args: args{ + path: "metadata.name", + value: "cool", + }, + want: want{ + object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "cool", + }, + }, + }, + }, + "ContainerName": { + reason: "Setting a field of an object that is an array element should work", + data: []byte(`{"spec":{"containers":[{"name":"lame"}]}}`), + args: args{ + path: "spec.containers[0].name", + value: "cool", + }, + want: want{ + object: map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "cool", + }, + }, + }, + }, + }, + }, + "NonExistentContainerName": { + reason: "Setting a field of a non-existent object that is an array element should work", + data: []byte(`{}`), + args: args{ + path: "spec.containers[0].name", + value: "cool", + }, + want: want{ + object: map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "cool", + }, + }, + }, + }, + }, + }, + "NewContainer": { + reason: "Growing an array object field should work", + data: []byte(`{"spec":{"containers":[{"name":"cool"}]}}`), + args: args{ + path: "spec.containers[1].name", + value: "cooler", + }, + want: want{ + object: map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "cool", + }, + map[string]interface{}{ + "name": "cooler", + }, + }, + }, + }, + }, + }, + "NestedArray": { + reason: "Setting a value in a nested array should work", + data: []byte(`{}`), + args: args{ + path: "data[0][0]", + value: "a", + }, + want: want{ + object: map[string]interface{}{ + "data": []interface{}{ + []interface{}{"a"}, + }, + }, + }, + }, + "GrowNestedArray": { + reason: "Growing then setting a value in a nested array should work", + data: []byte(`{"data":[["a"]]}`), + args: args{ + path: "data[0][1]", + value: "b", + }, + want: want{ + object: map[string]interface{}{ + "data": []interface{}{ + []interface{}{"a", "b"}, + }, + }, + }, + }, + "GrowArrayField": { + reason: "Growing then setting a value in an array field should work", + data: []byte(`{"data":["a"]}`), + args: args{ + path: "data[2]", + value: "c", + }, + want: want{ + object: map[string]interface{}{ + "data": []interface{}{"a", nil, "c"}, + }, + }, + }, + "NotAnArray": { + reason: "Indexing an object field should fail", + data: []byte(`{"data":{}}`), + args: args{ + path: "data[0]", + }, + want: want{ + object: map[string]interface{}{"data": map[string]interface{}{}}, + err: errors.New("data is not an array"), + }, + }, + "NotAnObject": { + reason: "Requesting a field in an array should fail", + data: []byte(`{"data":[]}`), + args: args{ + path: "data.name", + }, + want: want{ + object: map[string]interface{}{"data": []interface{}{}}, + err: errors.New("data is not an object"), + }, + }, + "MalformedPath": { + reason: "Requesting an invalid field path should fail", + args: args{ + path: "spec[]", + }, + want: want{ + object: map[string]interface{}{}, + err: errors.Wrap(errors.New("unexpected ']' at position 5"), "cannot parse path \"spec[]\""), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + in := make(map[string]interface{}) + _ = json.Unmarshal(tc.data, &in) + p := Pave(in) + + err := p.SetValue(tc.args.path, tc.args.value) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Fatalf("\np.SetValue(%s, %v): %s: -want error, +got error:\n%s", tc.args.path, tc.args.value, tc.reason, diff) + } + if diff := cmp.Diff(tc.want.object, p.object); diff != "" { + t.Fatalf("\np.SetValue(%s, %v): %s: -want, +got:\n%s", tc.args.path, tc.args.value, tc.reason, diff) + } + }) + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-vfvj-3m3g-m532ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-27483ghsaADVISORY
- github.com/crossplane/crossplane-runtime/commit/53508a9f4374604db140dd8ab2fa52276441e738ghsax_refsource_MISCWEB
- github.com/crossplane/crossplane-runtime/commit/f67177024d906aaf5e13ee7cd470b4e87a9fef40ghsaWEB
- github.com/crossplane/crossplane-runtime/security/advisories/GHSA-vfvj-3m3g-m532ghsax_refsource_CONFIRMWEB
- pkg.go.dev/vuln/GO-2023-1623ghsaWEB
News mentions
0No linked articles in our index yet.