From 96b80e54c75448e105bd47883bb196e00d5f8578 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sun, 11 Oct 2026 14:58:18 -0700 Subject: [PATCH] fix: reject Arazzo exponents that panic schema checks big.Rat.SetString rejects a decimal exponent above 1000000. The normalizer only compared the exponent to the byte budget, so a value such as retryAfter: 1e1000001 reached JSON Schema minimum checks and nil-dereferenced. Return the existing limit error instead. An exponent of 1000000 still validates. --- arazzo/nodes.go | 12 +++++++++--- arazzo/validate_test.go | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/arazzo/nodes.go b/arazzo/nodes.go index b5d689f..cc162f7 100644 --- a/arazzo/nodes.go +++ b/arazzo/nodes.go @@ -14,6 +14,10 @@ import ( "github.com/pb33f/go-yaml" ) +// maxSchemaExponent is the largest absolute decimal exponent math/big.Rat +// SetString accepts. One past this value makes JSON Schema minimum checks panic. +const maxSchemaExponent = 1_000_000 + func object(v any) map[string]any { m, _ := v.(map[string]any); return m } func array(v any) []any { a, _ := v.([]any); return a } func text(v any) string { s, _ := v.(string); return s } @@ -144,12 +148,14 @@ func (n *normalizer) value(node *yaml.Node, path string, depth int) (any, error) } // JSON's number grammar rejects NaN, infinity and YAML-only spellings. if json.Valid([]byte(clean)) && len(clean) > 0 && (clean[0] == '-' || clean[0] >= '0' && clean[0] <= '9') { - // Schema integer and numeric checks use arbitrary-precision numbers. - // Charge exponent expansion before those checks allocate 10^exponent. + // Schema numeric checks parse with big.Rat. SetString rejects a + // decimal exponent above 1_000_000, then panics comparing minimum + // or maximum. The byte budget is much larger, so it does not + // stop those values. Charge both limits before validation. if i := strings.IndexAny(clean, "eE"); i >= 0 { exponent := strings.TrimLeft(clean[i+1:], "+-") growth, err := strconv.Atoi(exponent) - if err != nil || growth > n.limits.MaxBytes-n.bytes { + if err != nil || growth > maxSchemaExponent || growth > n.limits.MaxBytes-n.bytes { return nil, &Error{Kind: ErrorLimit, Cause: fmt.Errorf("numeric exponent exceeds expanded byte limit")} } n.bytes += growth diff --git a/arazzo/validate_test.go b/arazzo/validate_test.go index 7948079..22e7b07 100644 --- a/arazzo/validate_test.go +++ b/arazzo/validate_test.go @@ -335,6 +335,10 @@ func TestFoundationCapsBeforeCompilation(t *testing.T) { }{ {strings.Replace(foundationYAML("1.1.0"), " steps:", " inputs: {type: string, pattern: 'a very long pattern'}\n steps:", 1), func() Limits { l := DefaultLimits(); l.MaxPatternBytes = 4; return l }()}, {strings.Replace(foundationYAML("1.1.0"), "operationId: read", "operationId: read, requestBody: {payload: 1e99999999999999}", 1), DefaultLimits()}, + // big.Rat.SetString accepts a decimal exponent of 1000000 and rejects 1000001. + // The byte budget is 16MiB, so this value used to reach schema minimum checks and panic. + {strings.Replace(foundationYAML("1.1.0"), "operationId: read", "operationId: read, onFailure: [{name: again, type: retry, retryAfter: 1e1000001}]", 1), DefaultLimits()}, + {strings.Replace(foundationYAML("1.1.0"), "operationId: read", "operationId: read, onFailure: [{name: again, type: retry, retryAfter: 1e-1000001}]", 1), DefaultLimits()}, {strings.Replace(foundationYAML("1.1.0"), " steps:", " x-a: {x-b: {x-c: {x-d: value}}}\n steps:", 1), func() Limits { l := DefaultLimits(); l.MaxDepth = 4; return l }()}, } for i, tt := range tests { @@ -344,6 +348,11 @@ func TestFoundationCapsBeforeCompilation(t *testing.T) { t.Fatalf("case %d: %+v %v", i, r, err) } } + accepted := strings.Replace(foundationYAML("1.1.0"), "operationId: read", "operationId: read, onFailure: [{name: again, type: retry, retryAfter: 1e1000000}]", 1) + r, err := ValidateBytes(context.Background(), []byte(accepted), "input.yaml") + if err != nil || !r.Valid() { + t.Fatalf("exponent 1000000 is representable: %+v %v", r, err) + } } // cloneNodeGraph snapshots caller-owned node graphs for immutability tests,