fix(deps): replace gopkg.in/yaml.v3 with github.com/goccy/go-yaml #89

Merged
aweiker merged 13 commits from review-bot-issue-87 into main 2026-05-13 03:47:02 +00:00
4 changed files with 93 additions and 64 deletions
Showing only changes of commit 8991260333 - Show all commits
+1 -1
View File
@@ -2,4 +2,4 @@ module gitea.weiker.me/rodin/review-bot
go 1.26.2
Review

[MAJOR] The repository CONVENTIONS.md defines a strict allowlist of approved third-party packages. The only approved YAML package is gopkg.in/yaml.v3. The package github.com/goccy/go-yaml is NOT on the allowlist. Per the documented process: 'To request a new dependency: 1. Open a PR that ONLY updates this table, 2. Requires explicit approval from Aaron, 3. After merge, a separate PR may use the package.' This PR skips that process entirely. Even if the intent is correct (issue #57 may have specified goccy/go-yaml), the dependency gate must be updated first in a separate PR before this change can be merged.

**[MAJOR]** The repository CONVENTIONS.md defines a strict allowlist of approved third-party packages. The only approved YAML package is `gopkg.in/yaml.v3`. The package `github.com/goccy/go-yaml` is NOT on the allowlist. Per the documented process: 'To request a new dependency: 1. Open a PR that ONLY updates this table, 2. Requires explicit approval from Aaron, 3. After merge, a separate PR may use the package.' This PR skips that process entirely. Even if the intent is correct (issue #57 may have specified goccy/go-yaml), the dependency gate must be updated first in a separate PR before this change can be merged.
Review

[MAJOR] Introduces unapproved third-party dependency github.com/goccy/go-yaml, violating the repository's strict dependency allowlist (only gopkg.in/yaml.v3 and github.com/google/go-cmp are permitted). This bypasses established supply chain security controls.

**[MAJOR]** Introduces unapproved third-party dependency github.com/goccy/go-yaml, violating the repository's strict dependency allowlist (only gopkg.in/yaml.v3 and github.com/google/go-cmp are permitted). This bypasses established supply chain security controls.
Review

[MAJOR] The repository conventions define a strict allowlist of approved third-party packages. github.com/goccy/go-yaml is NOT on the allowlist. The only approved YAML package is gopkg.in/yaml.v3. Per conventions: 'To request a new dependency: 1. Open a PR that ONLY updates this table. 2. Requires explicit approval from Aaron. 3. After merge, a separate PR may use the package.' This PR skips that process entirely.

**[MAJOR]** The repository conventions define a strict allowlist of approved third-party packages. `github.com/goccy/go-yaml` is NOT on the allowlist. The only approved YAML package is `gopkg.in/yaml.v3`. Per conventions: 'To request a new dependency: 1. Open a PR that ONLY updates this table. 2. Requires explicit approval from Aaron. 3. After merge, a separate PR may use the package.' This PR skips that process entirely.
Review

[MAJOR] Introduces github.com/goccy/go-yaml, which is not in the repository's approved third-party dependency allowlist. Per the repository conventions, adding a new dependency requires a separate PR that ONLY updates the allowlist table and receives explicit approval before usage.

**[MAJOR]** Introduces github.com/goccy/go-yaml, which is not in the repository's approved third-party dependency allowlist. Per the repository conventions, adding a new dependency requires a separate PR that ONLY updates the allowlist table and receives explicit approval before usage.
require gopkg.in/yaml.v3 v3.0.1
require github.com/goccy/go-yaml v1.19.2
Review

[MAJOR] Introduces a non-allowlisted dependency github.com/goccy/go-yaml, which violates the repository’s STRICT ALLOWLIST policy. Update the approved dependencies table before using this package.

**[MAJOR]** Introduces a non-allowlisted dependency github.com/goccy/go-yaml, which violates the repository’s STRICT ALLOWLIST policy. Update the approved dependencies table before using this package.
Review

[MAJOR] New dependency github.com/goccy/go-yaml is introduced but not present in the repository's strict allowlist of approved packages. This violates the dependency policy and presents a supply chain risk.

**[MAJOR]** New dependency github.com/goccy/go-yaml is introduced but not present in the repository's strict allowlist of approved packages. This violates the dependency policy and presents a supply chain risk.
+2 -4
View File
@@ -1,4 +1,2 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+61 -28
View File
@@ -10,7 +10,9 @@ import (
"strings"
"unicode/utf8"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/ast"
"github.com/goccy/go-yaml/parser"
)
//go:embed personas/*.yaml
@@ -142,7 +144,7 @@ func parsePersona(data []byte, source string) (*Persona, error) {
err = unmarshalYAMLWithDepthLimit(data, &p, MaxYAMLDepth)
} else {
// Use json.Decoder with DisallowUnknownFields for consistency with
// YAML's KnownFields(true) - both reject unknown fields to catch typos.
// YAML's Strict() - both reject unknown fields to catch typos.
dec := json.NewDecoder(bytes.NewReader(data))
Review

[MINOR] JSON decoding disallows unknown fields but does not verify EOF to catch trailing data after a valid JSON object. Consider an additional decode into a dummy value to ensure no extra tokens remain.

**[MINOR]** JSON decoding disallows unknown fields but does not verify EOF to catch trailing data after a valid JSON object. Consider an additional decode into a dummy value to ensure no extra tokens remain.
dec.DisallowUnknownFields()
err = dec.Decode(&p)
Review

[MINOR] The JSON trailing content check uses dec.More(), which is only meaningful inside arrays/objects and may be confusing at top level. Relying solely on attempting a second Decode and expecting io.EOF is sufficient and simpler.

**[MINOR]** The JSON trailing content check uses dec.More(), which is only meaningful inside arrays/objects and may be confusing at top level. Relying solely on attempting a second Decode and expecting io.EOF is sufficient and simpler.
1
@@ -161,39 +163,43 @@ func parsePersona(data []byte, source string) (*Persona, error) {
// nested structures and catches typos in field names.
// Multi-document YAML files are rejected to prevent silent data loss.
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
// First pass: decode into a yaml.Node to check depth limits and node counts.
// This prevents stack exhaustion before we attempt to decode into structs.
var node yaml.Node
dec := yaml.NewDecoder(bytes.NewReader(data))
if err := dec.Decode(&node); err != nil {
// First pass: parse into AST to check depth limits, node counts, and
// multi-document rejection. This prevents stack exhaustion before we
// attempt to decode into structs.
file, err := parser.ParseBytes(data, 0)
if err != nil {
return err
}
// Reject multi-document YAML files - silently ignoring additional documents
// could lead to confusing behavior where users think their changes take effect.
var extra yaml.Node
if dec.Decode(&extra) == nil {
if len(file.Docs) > 1 {
return fmt.Errorf("multi-document YAML is not supported; only single-document files are allowed")
}
if len(file.Docs) == 0 {
return fmt.Errorf("empty YAML document")
}
nodeCount := 0
if err := checkYAMLDepth(&node, 0, maxDepth, MaxYAMLNodes, make(map[*yaml.Node]struct{}), &nodeCount); err != nil {
if err := checkYAMLDepth(file.Docs[0].Body, 0, maxDepth, MaxYAMLNodes, make(map[ast.Node]struct{}), &nodeCount); err != nil {
return err
Review

[MINOR] The go 1.26.2 in go.mod is a pre-release/future version (Go 1.26 has not been released). This may cause go mod tidy to behave unexpectedly on stable Go toolchains and suggests the module was initialized with an unstable toolchain. This is pre-existing and not introduced by this PR, but worth flagging.

**[MINOR]** The `go 1.26.2` in go.mod is a pre-release/future version (Go 1.26 has not been released). This may cause `go mod tidy` to behave unexpectedly on stable Go toolchains and suggests the module was initialized with an unstable toolchain. This is pre-existing and not introduced by this PR, but worth flagging.
}
// Second pass: decode with strict field checking enabled.
// KnownFields(true) rejects unknown keys, catching typos like "focuss" or "identiy".
// We must re-decode from the original data because yaml.Node.Decode() doesn't
// support the KnownFields option.
strictDec := yaml.NewDecoder(bytes.NewReader(data))
strictDec.KnownFields(true)
return strictDec.Decode(out)
// Strict() rejects unknown keys, catching typos like "focuss" or "identiy".
dec := yaml.NewDecoder(bytes.NewReader(data), yaml.Strict())
return dec.Decode(out)
}
// checkYAMLDepth recursively checks that YAML nodes don't exceed the depth limit
// or the total node count limit. It also detects alias cycles to prevent infinite
// recursion from crafted YAML with self-referential aliases.
func checkYAMLDepth(node *yaml.Node, depth, maxDepth, maxNodes int, seen map[*yaml.Node]struct{}, nodeCount *int) error {
// checkYAMLDepth recursively checks that YAML AST nodes don't exceed the depth
Review

[NIT] The comment on unmarshalYAMLWithDepthLimit says 'Multi-document YAML files are rejected to prevent confusing behavior where additional documents are silently ignored.' but the function header also mentions 'strict field checking'. The doc comment could be tightened to mention all three concerns (depth, multi-doc, strict fields) in the opening summary rather than leaving strict-field checking implicit.

**[NIT]** The comment on `unmarshalYAMLWithDepthLimit` says 'Multi-document YAML files are rejected to prevent confusing behavior where additional documents are silently ignored.' but the function header also mentions 'strict field checking'. The doc comment could be tightened to mention all three concerns (depth, multi-doc, strict fields) in the opening summary rather than leaving strict-field checking implicit.
// limit or the total node count limit. It also detects alias cycles to prevent
// infinite recursion from crafted YAML with self-referential aliases.
Review

[MINOR] The checkYAMLDepth function increments *nodeCount before the depth-aware short-circuit check (validated map lookup). This means a node revisited at a shallower depth (which immediately returns via the short-circuit) still increments the counter. The comment acknowledges this as 'intentional conservative overcounting', which is a reasonable security posture. However, it also means the node count can be inflated by the number of times aliases reference the same shallow subtree, potentially causing false positives for legitimate YAML with many alias references to the same anchor. This is a trade-off that is documented and defensible, but worth noting.

**[MINOR]** The `checkYAMLDepth` function increments `*nodeCount` before the depth-aware short-circuit check (`validated` map lookup). This means a node revisited at a shallower depth (which immediately returns via the short-circuit) still increments the counter. The comment acknowledges this as 'intentional conservative overcounting', which is a reasonable security posture. However, it also means the node count can be inflated by the number of times aliases reference the same shallow subtree, potentially causing false positives for legitimate YAML with many alias references to the same anchor. This is a trade-off that is documented and defensible, but worth noting.
Review

[MINOR] The unmarshalYAMLWithDepthLimit function does a two-pass approach: first parser.ParseBytes then yaml.NewDecoder(...).Decode(out). This means the raw bytes are parsed twice. The second decode with yaml.Strict() also re-parses from a bytes.NewReader(data). For the file size constraint (64KB max), this is acceptable, but worth noting. A minor concern is that the two passes could theoretically behave differently if the library has any parse-then-decode divergence. However, this is standard practice for AST-pre-validation patterns and is not a bug.

**[MINOR]** The `unmarshalYAMLWithDepthLimit` function does a two-pass approach: first `parser.ParseBytes` then `yaml.NewDecoder(...).Decode(out)`. This means the raw bytes are parsed twice. The second decode with `yaml.Strict()` also re-parses from a `bytes.NewReader(data)`. For the file size constraint (64KB max), this is acceptable, but worth noting. A minor concern is that the two passes could theoretically behave differently if the library has any parse-then-decode divergence. However, this is standard practice for AST-pre-validation patterns and is not a bug.
func checkYAMLDepth(node ast.Node, depth, maxDepth, maxNodes int, seen map[ast.Node]struct{}, nodeCount *int) error {
if node == nil {
Review

[NIT] The unmarshalYAMLWithDepthLimit function takes maxDepth int as a parameter but always calls checkYAMLDepth with the package-level constant MaxYAMLNodes directly rather than accepting it as a parameter. This asymmetry is mildly inconsistent — either both limits should be constants used directly, or both should be parameters. Low impact since callers always pass MaxYAMLDepth anyway.

**[NIT]** The `unmarshalYAMLWithDepthLimit` function takes `maxDepth int` as a parameter but always calls `checkYAMLDepth` with the package-level constant `MaxYAMLNodes` directly rather than accepting it as a parameter. This asymmetry is mildly inconsistent — either both limits should be constants used directly, or both should be parameters. Low impact since callers always pass `MaxYAMLDepth` anyway.
return nil
Review

[MINOR] checkYAMLDepth breaks alias cycles by returning nil (skipping the cyclic subtree). Consider returning a specific error on detected cycles to fail-fast instead of relying on downstream decoder behavior, improving safety and transparency for malicious inputs.

**[MINOR]** checkYAMLDepth breaks alias cycles by returning nil (skipping the cyclic subtree). Consider returning a specific error on detected cycles to fail-fast instead of relying on downstream decoder behavior, improving safety and transparency for malicious inputs.
}
if depth > maxDepth {
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
}
Review

[MINOR] The unmarshalYAMLWithDepthLimit function performs a two-pass decode (AST parse then full decode). The second pass with yaml.Strict() will also parse the YAML from scratch, so the file bytes are parsed twice. This is intentional per the comment but worth noting: if the goccy/go-yaml Strict() decoder also does alias resolution internally, the depth protection from the first pass only guards the structural AST walk, not the decoder's internal expansion. This appears acceptable given the design, but should be validated that yaml.Strict() doesn't recurse unboundedly on crafted alias chains during decode.

**[MINOR]** The `unmarshalYAMLWithDepthLimit` function performs a two-pass decode (AST parse then full decode). The second pass with `yaml.Strict()` will also parse the YAML from scratch, so the file bytes are parsed twice. This is intentional per the comment but worth noting: if the goccy/go-yaml `Strict()` decoder also does alias resolution internally, the depth protection from the first pass only guards the structural AST walk, not the decoder's internal expansion. This appears acceptable given the design, but should be validated that `yaml.Strict()` doesn't recurse unboundedly on crafted alias chains during decode.
Review

Already addressed. The comment block immediately above the dec := yaml.NewDecoder(...) call explicitly documents this:

Safety note: goccy/go-yaml's decoder does not expand YAML aliases recursively — it resolves them via the pre-built AST, which our first pass already depth-checked. Alias chains that would exceed depth limits are caught above; the decoder merely reads the resolved scalar values.

The two-pass design is intentional: pass 1 validates structure/depth on the AST (where we have full control), pass 2 uses Strict() for field validation (which doesn't re-expand aliases recursively). No change needed.

Already addressed. The comment block immediately above the `dec := yaml.NewDecoder(...)` call explicitly documents this: > Safety note: goccy/go-yaml's decoder does not expand YAML aliases recursively — it resolves them via the pre-built AST, which our first pass already depth-checked. Alias chains that would exceed depth limits are caught above; the decoder merely reads the resolved scalar values. The two-pass design is intentional: pass 1 validates structure/depth on the AST (where we have full control), pass 2 uses `Strict()` for field validation (which doesn't re-expand aliases recursively). No change needed.
2
@@ -210,17 +216,44 @@ func checkYAMLDepth(node *yaml.Node, depth, maxDepth, maxNodes int, seen map[*ya
}
seen[node] = struct{}{}
// Handle alias nodes: follow the alias to its anchor target.
// Increment depth when following aliases since they expand the effective structure.
if node.Kind == yaml.AliasNode && node.Alias != nil {
return checkYAMLDepth(node.Alias, depth+1, maxDepth, maxNodes, seen, nodeCount)
}
for _, child := range node.Content {
if err := checkYAMLDepth(child, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
// Walk children based on node type.
switch n := node.(type) {
case *ast.MappingNode:
for _, value := range n.Values {
if err := checkYAMLDepth(value, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
return err
Review

[MINOR] The validated map stores depth as the value but is described as storing 'the maximum depth at which it was previously checked.' The current code stores the current visit depth, not the maximum. If a node is first visited at depth 10 and later at depth 5, validated[node] becomes 5 — the shorter path. The short-circuit condition depth <= prevDepth then allows re-traversal at depth 6 even though we already checked at depth 10. In practice this isn't a security issue (the alias depth bypass test covers the real attack vector), but the comment and variable name ('validated' implying completeness) are slightly misleading. Consider renaming to visitedAtDepth and updating the comment to say 'the last depth at which this node was validated' rather than 'maximum'.

**[MINOR]** The `validated` map stores `depth` as the value but is described as storing 'the maximum depth at which it was previously checked.' The current code stores the *current* visit depth, not the maximum. If a node is first visited at depth 10 and later at depth 5, `validated[node]` becomes 5 — the shorter path. The short-circuit condition `depth <= prevDepth` then allows re-traversal at depth 6 even though we already checked at depth 10. In practice this isn't a security issue (the alias depth bypass test covers the real attack vector), but the comment and variable name ('validated' implying completeness) are slightly misleading. Consider renaming to `visitedAtDepth` and updating the comment to say 'the last depth at which this node was validated' rather than 'maximum'.
}
Review

[MAJOR] checkYAMLDepth does not handle ast.MergeKeyNode and treats it as a leaf (per the default case comment). YAML merge keys (<<) can contain aliases to mappings; not traversing MergeKeyNode children can bypass the depth enforcement when deep structures are merged, undermining the DoS protections. Add an explicit case to traverse MergeKeyNode's referenced values (typically aliases) and continue depth/node counting.

**[MAJOR]** checkYAMLDepth does not handle ast.MergeKeyNode and treats it as a leaf (per the default case comment). YAML merge keys (<<) can contain aliases to mappings; not traversing MergeKeyNode children can bypass the depth enforcement when deep structures are merged, undermining the DoS protections. Add an explicit case to traverse MergeKeyNode's referenced values (typically aliases) and continue depth/node counting.
}
case *ast.MappingValueNode:
if err := checkYAMLDepth(n.Key, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
return err
}
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
return err
}
case *ast.SequenceNode:
Review

[MINOR] The validated map stores the depth at which a node was last validated, used for depth-aware short-circuiting. However, the map is keyed by ast.Node (interface type). Interface map keys use pointer identity for concrete pointer types, which is correct here. But the comment says "maps each node to the minimum depth" — the map is actually storing the current visit's depth (overwriting each time the node is visited at a greater depth), not the minimum. The comment should say "most recently validated depth" or "maximum depth at which validated" to avoid confusion.

**[MINOR]** The `validated` map stores the depth at which a node was last validated, used for depth-aware short-circuiting. However, the map is keyed by `ast.Node` (interface type). Interface map keys use pointer identity for concrete pointer types, which is correct here. But the comment says "maps each node to the minimum depth" — the map is actually storing the *current visit's* depth (overwriting each time the node is visited at a greater depth), not the minimum. The comment should say "most recently validated depth" or "maximum depth at which validated" to avoid confusion.
for _, value := range n.Values {
if err := checkYAMLDepth(value, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
return err
}
}
case *ast.AliasNode:
// Follow alias to its target, incrementing depth since aliases expand
// the effective structure.
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
return err
}
case *ast.AnchorNode:
Review

[MINOR] The validated map uses ast.Node (interface) as a key, which relies on pointer equality for interface comparison. This works correctly for pointer types like *ast.MappingNode, but if any ast.Node implementation is a value type (non-pointer), two structurally identical nodes could hash to different entries or the same entry incorrectly. In practice, all goccy/go-yaml AST node types appear to be pointer types, so this is low risk, but it's an implicit assumption worth a comment.

**[MINOR]** The `validated` map uses `ast.Node` (interface) as a key, which relies on pointer equality for interface comparison. This works correctly for pointer types like `*ast.MappingNode`, but if any `ast.Node` implementation is a value type (non-pointer), two structurally identical nodes could hash to different entries or the same entry incorrectly. In practice, all goccy/go-yaml AST node types appear to be pointer types, so this is low risk, but it's an implicit assumption worth a comment.
Review

Already addressed. There's an explicit comment at the validated map access (the depth-aware short-circuit block):

Note: using ast.Node (interface) as map key relies on pointer identity, which is correct because all goccy/go-yaml AST node types are pointer receivers (*MappingNode, *SequenceNode, etc.), never value types.

This documents the assumption directly at the usage site. No change needed.

Already addressed. There's an explicit comment at the `validated` map access (the depth-aware short-circuit block): > Note: using ast.Node (interface) as map key relies on pointer identity, which is correct because all goccy/go-yaml AST node types are pointer receivers (*MappingNode, *SequenceNode, etc.), never value types. This documents the assumption directly at the usage site. No change needed.
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
Review

[NIT] The checkYAMLDepth function receives maxNodes int as a parameter but MaxYAMLNodes is passed as the constant from the call site. The maxNodes parameter is never varied between call sites — it's always MaxYAMLNodes. Could simplify by using the constant directly in the function, but having it as a parameter makes testing easier (allows injecting lower limits in tests). Current approach is acceptable.

**[NIT]** The `checkYAMLDepth` function receives `maxNodes int` as a parameter but `MaxYAMLNodes` is passed as the constant from the call site. The `maxNodes` parameter is never varied between call sites — it's always `MaxYAMLNodes`. Could simplify by using the constant directly in the function, but having it as a parameter makes testing easier (allows injecting lower limits in tests). Current approach is acceptable.
return err
}
case *ast.TagNode:
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil {
return err
}
// Scalar types (StringNode, IntegerNode, FloatNode, BoolNode, NullNode,
// InfinityNode, NanNode, LiteralNode, MergeKeyNode) are leaf nodes.
}
return nil
}
Review

[MINOR] The MappingValueNode case visits both Key and Value at depth+1 relative to the MappingValueNode's own depth. Since MappingValueNode is itself visited at depth+1 from its parent MappingNode, keys end up at depth+2 from the mapping. This asymmetry between key depth and value depth means scalar keys consume an extra depth level relative to what might be intuitive, but it's consistent and the tests validate the behavior works. Worth a brief comment explaining that keys consume a depth level intentionally.

**[MINOR]** The `MappingValueNode` case visits both Key and Value at `depth+1` relative to the MappingValueNode's own depth. Since `MappingValueNode` is itself visited at `depth+1` from its parent `MappingNode`, keys end up at `depth+2` from the mapping. This asymmetry between key depth and value depth means scalar keys consume an extra depth level relative to what might be intuitive, but it's consistent and the tests validate the behavior works. Worth a brief comment explaining that keys consume a depth level intentionally.
2
+29 -31
View File
@@ -7,7 +7,7 @@ import (
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
"strings"
"testing"
"gopkg.in/yaml.v3"
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
"github.com/goccy/go-yaml/ast"
Review

[MAJOR] Tests import github.com/goccy/go-yaml/ast, which is also subject to the dependency allowlist and currently not approved (only github.com/google/go-cmp is allowed for tests).

**[MAJOR]** Tests import github.com/goccy/go-yaml/ast, which is also subject to the dependency allowlist and currently not approved (only github.com/google/go-cmp is allowed for tests).
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
)
func TestLoadBuiltinPersona(t *testing.T) {
security-review-bot marked this conversation as resolved
Review

[MINOR] Tests import github.com/goccy/go-yaml/ast, which is also not in the approved dependency allowlist. Even for tests, third-party dependencies must be explicitly approved.

**[MINOR]** Tests import github.com/goccy/go-yaml/ast, which is also not in the approved dependency allowlist. Even for tests, third-party dependencies must be explicitly approved.
@@ -459,7 +459,7 @@ func TestYAMLDeeplyNestedRejection(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
path := filepath.Join(dir, "deeply-nested.yaml")
// Build a deeply nested YAML structure that exceeds MaxYAMLDepth (20).
Review

[MINOR] TestYAMLDeeplyNestedRejection comment says 'With 25 levels, we exceed MaxYAMLDepth (20)' but the depth counting depends on both MappingNode and MappingValueNode traversal (each contributes depth+1 for key, depth+1 for value). The comment would benefit from a concrete trace of how the depth accumulates to make the test self-documenting and easier to maintain if MaxYAMLDepth changes.

**[MINOR]** TestYAMLDeeplyNestedRejection comment says 'With 25 levels, we exceed MaxYAMLDepth (20)' but the depth counting depends on both MappingNode and MappingValueNode traversal (each contributes depth+1 for key, depth+1 for value). The comment would benefit from a concrete trace of how the depth accumulates to make the test self-documenting and easier to maintain if MaxYAMLDepth changes.
// Each level adds 2 to the depth count (key + value mapping).
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Each level adds to the depth count via mapping values.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
var sb strings.Builder
sb.WriteString("name: test\nidentity: test\nnested:\n")
indent := " "
11
@@ -505,30 +505,29 @@ func TestYAMLFileSizeLimit(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestYAMLAliasCycleDetection(t *testing.T) {
// Test that our checkYAMLDepth function handles alias cycles gracefully
// by using the seen map to prevent infinite recursion.
// We test this directly because go-yaml's parser handles most cycles
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// at parse time, but we need to ensure our checker is robust.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Create a node structure where an alias points to a parent node,
// simulating what could happen with malicious input that bypasses
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// go-yaml's cycle detection.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
parent := &yaml.Node{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.MappingNode,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Content: []*yaml.Node{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "name"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "test"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "nested"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// simulating what could happen with crafted input.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
parent := &ast.MappingNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Values: []*ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Key: &ast.StringNode{Value: "name"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: &ast.StringNode{Value: "test"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
}
// Create a child that aliases back to the parent (artificial cycle)
aliasToParent := &yaml.Node{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.AliasNode,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Alias: parent,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
aliasToParent := &ast.AliasNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: parent,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
parent.Content = append(parent.Content, aliasToParent)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
parent.Values = append(parent.Values, &ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

[NIT] TestYAMLEmptyFileRejection has a redundant if err != nil guard before checking err.Error() — since the previous block already verified err != nil, the outer guard is unnecessary. This is a style nit that doesn't affect correctness.

**[NIT]** `TestYAMLEmptyFileRejection` has a redundant `if err != nil` guard before checking `err.Error()` — since the previous block already verified `err != nil`, the outer guard is unnecessary. This is a style nit that doesn't affect correctness.
Key: &ast.StringNode{Value: "nested"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: aliasToParent,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
nodeCount := 0
seen := make(map[*yaml.Node]struct{})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
seen := make(map[ast.Node]struct{})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// This should NOT hang or stack overflow - the seen map prevents infinite recursion
err := checkYAMLDepth(parent, 0, MaxYAMLDepth, MaxYAMLNodes, seen, &nodeCount)
@@ -594,27 +593,26 @@ func TestYAMLNodeCountLimit(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestCheckYAMLDepthCycleDetectionDirect(t *testing.T) {
// Direct test of cycle detection in checkYAMLDepth by creating
// a node structure with an artificial cycle.
// This tests the seen map logic independent of go-yaml's parsing.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
node := &yaml.Node{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.MappingNode,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Content: []*yaml.Node{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "key"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "value"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
node := &ast.MappingNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Values: []*ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Key: &ast.StringNode{Value: "key"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: &ast.StringNode{Value: "value"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
}
// Create a cycle by making a child reference the parent
cycleChild := &yaml.Node{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.AliasNode,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Alias: node, // Points back to the parent
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
cycleChild := &ast.AliasNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: node, // Points back to the parent
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
node.Content = append(node.Content,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
&yaml.Node{Kind: yaml.ScalarNode, Value: "cyclic"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
cycleChild,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
node.Values = append(node.Values, &ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Key: &ast.StringNode{Value: "cyclic"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: cycleChild,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
nodeCount := 0
seen := make(map[*yaml.Node]struct{})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
seen := make(map[ast.Node]struct{})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
err := checkYAMLDepth(node, 0, MaxYAMLDepth, MaxYAMLNodes, seen, &nodeCount)
// Should complete without infinite recursion due to cycle detection
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.