fix(review): address feedback from reviews 2788, 2789, 2791
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 39s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m45s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m7s
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 39s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m45s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 2m7s
- Move nodeCount increment after cycle detection to avoid over-counting cyclic references (sonnet #2) - Use underscores in test case names used as filenames (sonnet #3) - Fix function comment: 'prevent silent data loss' → 'prevent confusing behavior where additional documents are silently ignored' (sonnet #4) - Mark design doc pseudocode as historical since implementation uses goccy/go-yaml ast.Node, not gopkg.in/yaml.v3 yaml.Node (sonnet #5)
This commit is contained in:
@@ -33,6 +33,11 @@ func parsePersona(data []byte, source string) (*Persona, error) {
|
|||||||
|
|
||||||
### YAML Parsing with Depth Protection
|
### YAML Parsing with Depth Protection
|
||||||
|
|
||||||
|
> **Note:** The pseudocode below reflects the initial design using `gopkg.in/yaml.v3`
|
||||||
|
> types (`yaml.Node`). The actual implementation uses `github.com/goccy/go-yaml`
|
||||||
|
> with `ast.Node`-based traversal, dual-map cycle/depth tracking, and node-count
|
||||||
|
> limits. See `review/persona.go` for the current implementation.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
|
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
|
||||||
var node yaml.Node
|
var node yaml.Node
|
||||||
|
|||||||
+9
-7
@@ -161,7 +161,8 @@ func parsePersona(data []byte, source string) (*Persona, error) {
|
|||||||
// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting
|
// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting
|
||||||
// and strict field checking. This protects against stack exhaustion from deeply
|
// and strict field checking. This protects against stack exhaustion from deeply
|
||||||
// nested structures and catches typos in field names.
|
// nested structures and catches typos in field names.
|
||||||
// Multi-document YAML files are rejected to prevent silent data loss.
|
// Multi-document YAML files are rejected to prevent confusing behavior
|
||||||
|
// where additional documents are silently ignored.
|
||||||
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
|
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
|
||||||
// First pass: parse into AST to check depth limits, node counts, and
|
// First pass: parse into AST to check depth limits, node counts, and
|
||||||
// multi-document rejection. This prevents stack exhaustion before we
|
// multi-document rejection. This prevents stack exhaustion before we
|
||||||
@@ -214,12 +215,6 @@ func checkYAMLDepth(node ast.Node, depth, maxDepth, maxNodes int, validated map[
|
|||||||
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
|
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track total nodes visited as defense-in-depth against wide-but-shallow attacks.
|
|
||||||
*nodeCount++
|
|
||||||
if *nodeCount > maxNodes {
|
|
||||||
return fmt.Errorf("YAML node count exceeds maximum (%d)", maxNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cycle detection: if we're currently visiting this node on the current
|
// Cycle detection: if we're currently visiting this node on the current
|
||||||
// recursion path, it's a cycle (e.g., alias pointing to an ancestor).
|
// recursion path, it's a cycle (e.g., alias pointing to an ancestor).
|
||||||
// Return nil to break the cycle without error — cycles are a structural
|
// Return nil to break the cycle without error — cycles are a structural
|
||||||
@@ -228,6 +223,13 @@ func checkYAMLDepth(node ast.Node, depth, maxDepth, maxNodes int, validated map[
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track total nodes visited as defense-in-depth against wide-but-shallow attacks.
|
||||||
|
// Placed after cycle detection to avoid over-counting cyclic references.
|
||||||
|
*nodeCount++
|
||||||
|
if *nodeCount > maxNodes {
|
||||||
|
return fmt.Errorf("YAML node count exceeds maximum (%d)", maxNodes)
|
||||||
|
}
|
||||||
|
|
||||||
// Depth-aware short-circuit: only skip re-checking a node if we previously
|
// Depth-aware short-circuit: only skip re-checking a node if we previously
|
||||||
// validated it at the same or deeper effective depth. If this visit is at a
|
// validated it at the same or deeper effective depth. If this visit is at a
|
||||||
// greater depth than before (e.g., alias referenced deeper in the tree),
|
// greater depth than before (e.g., alias referenced deeper in the tree),
|
||||||
|
|||||||
@@ -491,9 +491,9 @@ func TestYAMLEmptyFileRejection(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
}{
|
}{
|
||||||
{"completely empty", ""},
|
{"completely_empty", ""},
|
||||||
{"whitespace only", " \n\n "},
|
{"whitespace_only", " \n\n "},
|
||||||
{"comment only", "# just a comment\n"},
|
{"comment_only", "# just a comment\n"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user