Skip to content

feat(validation): JSON-Schema compile timeout + depth pre-scan#256

Merged
zeevdr merged 1 commit into
mainfrom
feat/schema-compile-timeout
Apr 29, 2026
Merged

feat(validation): JSON-Schema compile timeout + depth pre-scan#256
zeevdr merged 1 commit into
mainfrom
feat/schema-compile-timeout

Conversation

@zeevdr

@zeevdr zeevdr commented Apr 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Follow-up to feat(schema): configurable ingest limits + functional options for NewService #254 (now merged). Lands the second half of schema: bound field count, doc size, JSON-Schema compile time #217: bound the JSON-Schema compiler so a malicious per-field json_schema constraint cannot hang the server on first compile.
  • New validation.Limits (CompileTimeout, MaxDepth) with a shared Option / WithLimits pattern — same option type works for both NewValidatorFactory and NewFieldValidator. DefaultLimits is 5 s + depth 64.
  • Pre-compile depth scan walks the parsed JSON-Schema document and rejects pathological nesting before invoking the compiler. The compile itself runs in a goroutine and is bounded by CompileTimeout. Because jsonschema/v6 has no CompileContext, the timeout is a wall-clock backstop — the goroutine may continue past the deadline, but the pre-scan plus the upstream MaxDocBytes cap (feat(schema): configurable ingest limits + functional options for NewService #254) bound the worst-case work.
  • cmd/server reads SCHEMA_COMPILE_TIMEOUT (Go duration) and SCHEMA_MAX_REF_DEPTH env vars and threads them through the shared validator factory.

(Replaces #255, which was auto-closed when its base branch feat/schema-limits was deleted on #254 merge.)

Test plan

  • make pre-commit (build, vet, format, lint, test, coverage)
  • New unit tests in internal/validation/json_schema_test.go cover: DefaultLimits, depth-exceeded rejection, depth-disabled (MaxDepth: 0), malformed-JSON falls through to compiler, timeout-zero-is-unbounded, and direct scanJSONDepth cases (objects, arrays, non-JSON)
  • Existing factory_concurrent_test.go / validator_bench_test.go / cache_race_test.go callers continue to compile (zero-opt form preserved)
  • Env vars SCHEMA_COMPILE_TIMEOUT and SCHEMA_MAX_REF_DEPTH documented in docs/server/configuration.md
  • .agents/context/security-review.md updated — finding 6 now marked complete across both halves

Closes #217.

🤖 Generated with Claude Code

Lands the second half of #217 (security review finding 6): bound the
JSON-Schema compiler so a malicious per-field constraint cannot hang
the server during the first compile. Sits on top of #254, which already
caps schema doc bytes and field count at the ingest layer.

- internal/validation/limits.go — new Limits struct (CompileTimeout,
  MaxDepth) + DefaultLimits (5 s, depth 64) + shared Option/WithLimits
  pattern usable by both the factory and individual field validators.
- internal/validation/json_schema.go — newJSONSchemaValidator now takes
  Limits. A pre-compile depth scan rejects pathological nesting before
  invoking the compiler; the Compile call itself runs in a goroutine
  and is bounded by CompileTimeout. jsonschema/v6 has no CompileContext,
  so the timeout is a wall-clock backstop — the goroutine may continue
  past the deadline, but the depth scan + upstream MaxDocBytes cap
  bound the worst-case work.
- NewValidatorFactory(store, opts...) and NewFieldValidator(..., opts...)
  accept the shared Option type; existing zero-opt call sites continue
  to compile unchanged.
- cmd/server reads SCHEMA_COMPILE_TIMEOUT (Go duration) and
  SCHEMA_MAX_REF_DEPTH env vars and threads them through WithLimits on
  the shared validator factory.

Closes #217.
@zeevdr zeevdr added this to the Security Review milestone Apr 29, 2026
@zeevdr zeevdr added enhancement New feature or request server Server changes size: M Moderate — a day or two, clear scope priority: P0 Blocks alpha or release labels Apr 29, 2026
@zeevdr zeevdr merged commit ad6470d into main Apr 29, 2026
19 checks passed
@zeevdr zeevdr deleted the feat/schema-compile-timeout branch April 29, 2026 11:30
zeevdr added a commit that referenced this pull request May 1, 2026
PR #256 added a wall-clock CompileTimeout to newJSONSchemaValidator. The
success and disabled-timeout branches were tested; the timeout-fired
branch was not.

Add TestNewJSONSchemaValidator_CompileTimeoutFires using a 1ns timeout
against a small valid schema. The timer.C is ready before the compile
goroutine can be scheduled and push to the buffered result channel, so
the select reaches the timeout branch deterministically (verified across
20 consecutive runs).

Closes #281

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 1, 2026
PR #254 added configurable schema ingest limits (MaxFields, MaxDocBytes)
and PR #256 added JSON-Schema compile-time bounds. The validators have
unit tests; this lands the live e2e coverage of the user-facing
rejection paths.

Lower the docker-compose service's caps to small values
(SCHEMA_MAX_FIELDS=100, SCHEMA_MAX_DOC_BYTES=4096) so the new tests can
trip them with cheap payloads. Existing fixtures use at most a few
fields and well under 1 KiB of YAML, so other e2e tests are unaffected.

Add three tests in e2e/validation_limits_test.go:

- TestValidationLimits_MaxFieldsRejected — CreateSchema with 101 fields
  returns InvalidArgument and the message cites the configured cap.
- TestValidationLimits_MaxFieldsAtLimitAccepted — exactly 100 fields is
  accepted; the cap is inclusive at the limit.
- TestValidationLimits_MaxDocBytesRejected — ImportSchema with a YAML
  body padded past the cap is rejected before the parser runs.

The JSON-Schema MaxDepth path is intentionally not covered here:
internal/validation/validator.go silently swallows newJSONSchemaValidator
errors at line 147 (constraints.JsonSchema branch), so a depth-exceeded
schema is accepted at CreateSchema and only surfaces later as a missing
JSON-Schema check. That swallow is worth a separate issue; the depth
trigger itself is unit-tested in internal/validation.

Closes #283

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 1, 2026
PR #256 added a wall-clock CompileTimeout to newJSONSchemaValidator. The
success and disabled-timeout branches were tested; the timeout-fired
branch was not.

Add TestNewJSONSchemaValidator_CompileTimeoutFires using a 1ns timeout
against a small valid schema. The timer.C is ready before the compile
goroutine can be scheduled and push to the buffered result channel, so
the select reaches the timeout branch deterministically (verified across
20 consecutive runs).

Closes #281

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
zeevdr added a commit that referenced this pull request May 3, 2026
* test(e2e): cover validation-limit rejections (#283)

PR #254 added configurable schema ingest limits (MaxFields, MaxDocBytes)
and PR #256 added JSON-Schema compile-time bounds. The validators have
unit tests; this lands the live e2e coverage of the user-facing
rejection paths.

Lower the docker-compose service's caps to small values
(SCHEMA_MAX_FIELDS=100, SCHEMA_MAX_DOC_BYTES=4096) so the new tests can
trip them with cheap payloads. Existing fixtures use at most a few
fields and well under 1 KiB of YAML, so other e2e tests are unaffected.

Add three tests in e2e/validation_limits_test.go:

- TestValidationLimits_MaxFieldsRejected — CreateSchema with 101 fields
  returns InvalidArgument and the message cites the configured cap.
- TestValidationLimits_MaxFieldsAtLimitAccepted — exactly 100 fields is
  accepted; the cap is inclusive at the limit.
- TestValidationLimits_MaxDocBytesRejected — ImportSchema with a YAML
  body padded past the cap is rejected before the parser runs.

The JSON-Schema MaxDepth path is intentionally not covered here:
internal/validation/validator.go silently swallows newJSONSchemaValidator
errors at line 147 (constraints.JsonSchema branch), so a depth-exceeded
schema is accepted at CreateSchema and only surfaces later as a missing
JSON-Schema check. That swallow is worth a separate issue; the depth
trigger itself is unit-tested in internal/validation.

Closes #283

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: P0 Blocks alpha or release server Server changes size: M Moderate — a day or two, clear scope

Projects

None yet

Development

Successfully merging this pull request may close these issues.

schema: bound field count, doc size, JSON-Schema compile time

1 participant