Skip to content

Commit 5152cb6

Browse files
committed
feat: add YAML anchors and aliases support to ClusterConfig
- Add resolveYAMLAnchors() function to process YAML anchors and aliases - Remove only 'aliases' field to maintain strict validation for other fields - Include security protections against YAML bombs and excessive nesting - Add comprehensive test coverage for functionality and edge cases - Update FAQ documentation with YAML anchor examples Fixes #8270
1 parent 5f55c80 commit 5152cb6

File tree

3 files changed

+406
-2
lines changed

3 files changed

+406
-2
lines changed

pkg/eks/api.go

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,18 +212,100 @@ func newAWSProvider(spec *api.ProviderConfig, configurationLoader AWSConfigurati
212212
return provider, nil
213213
}
214214

215+
// resolveYAMLAnchors processes YAML anchors and aliases, returning clean YAML
216+
// that can be safely parsed by strict unmarshaling. It removes the "aliases"
217+
// field commonly used for anchor definitions as it's not part of ClusterConfig schema.
218+
func resolveYAMLAnchors(data []byte) ([]byte, error) {
219+
// Security: Limit input size to prevent memory exhaustion attacks
220+
const maxInputSize = 1024 * 1024 // 1MB limit
221+
if len(data) > maxInputSize {
222+
return nil, fmt.Errorf("YAML input too large: %d bytes exceeds limit of %d bytes", len(data), maxInputSize)
223+
}
224+
225+
// Security: Check for excessive nesting depth to prevent stack overflow
226+
const maxNestingDepth = 10
227+
if nestingDepth := countNestingDepth(data); nestingDepth > maxNestingDepth {
228+
return nil, fmt.Errorf("YAML nesting too deep: %d levels exceeds limit of %d", nestingDepth, maxNestingDepth)
229+
}
230+
231+
// Resolve YAML anchors and aliases by unmarshaling to interface{} first.
232+
// This step processes any YAML anchors (&anchor) and aliases (*alias) in the input,
233+
// expanding them to their full values.
234+
var resolved interface{}
235+
if err := yaml.Unmarshal(data, &resolved); err != nil {
236+
return nil, err
237+
}
238+
239+
// Marshal back to get resolved YAML without anchors/aliases
240+
resolvedData, err := yaml.Marshal(resolved)
241+
if err != nil {
242+
return nil, err
243+
}
244+
245+
// Security: Check for excessive expansion (YAML bomb protection)
246+
const maxExpansionRatio = 10
247+
if len(resolvedData) > len(data)*maxExpansionRatio {
248+
return nil, fmt.Errorf("YAML expansion too large: %d bytes expanded from %d bytes (ratio: %d, limit: %d)",
249+
len(resolvedData), len(data), len(resolvedData)/len(data), maxExpansionRatio)
250+
}
251+
252+
// Remove the "aliases" field commonly used for YAML anchor definitions
253+
// as it's not part of the ClusterConfig schema
254+
var temp map[string]interface{}
255+
if err := yaml.Unmarshal(resolvedData, &temp); err != nil {
256+
return nil, err
257+
}
258+
259+
// Remove only the aliases field, maintaining strict validation for everything else
260+
delete(temp, "aliases")
261+
262+
// Marshal back to clean YAML
263+
return yaml.Marshal(temp)
264+
}
265+
266+
// countNestingDepth estimates YAML nesting depth by counting indentation
267+
func countNestingDepth(data []byte) int {
268+
lines := strings.Split(string(data), "\n")
269+
maxDepth := 0
270+
for _, line := range lines {
271+
if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
272+
continue
273+
}
274+
depth := 0
275+
for _, char := range line {
276+
if char == ' ' {
277+
depth++
278+
} else if char == '\t' {
279+
depth += 2 // Count tabs as 2 spaces
280+
} else {
281+
break
282+
}
283+
}
284+
if depth/2 > maxDepth { // Assuming 2-space indentation
285+
maxDepth = depth / 2
286+
}
287+
}
288+
return maxDepth
289+
}
290+
215291
// ParseConfig parses data into a ClusterConfig
216292
func ParseConfig(data []byte) (*api.ClusterConfig, error) {
293+
// Resolve YAML anchors and aliases before parsing
294+
cleanData, err := resolveYAMLAnchors(data)
295+
if err != nil {
296+
return nil, err
297+
}
298+
217299
// strict mode is not available in runtime.Decode, so we use the parser
218300
// directly; we don't store the resulting object, this is just the means
219301
// of detecting any unknown keys
220302
// NOTE: we must use sigs.k8s.io/yaml, as it behaves differently from
221303
// github.com/ghodss/yaml, which didn't handle nested structs well
222-
if err := yaml.UnmarshalStrict(data, &api.ClusterConfig{}); err != nil {
304+
if err := yaml.UnmarshalStrict(cleanData, &api.ClusterConfig{}); err != nil {
223305
return nil, err
224306
}
225307

226-
obj, err := runtime.Decode(scheme.Codecs.UniversalDeserializer(), data)
308+
obj, err := runtime.Decode(scheme.Codecs.UniversalDeserializer(), cleanData)
227309
if err != nil {
228310
return nil, err
229311
}

0 commit comments

Comments
 (0)