Harden response-contract validation (DoS) + mock routing; adversarial QA pass#12
Merged
Merged
Conversation
`respond()` returned the first route whose regex matched in document order, so
a literal path declared after a parametric one was unreachable — e.g. a request
to `/users/me` matched an earlier `/users/{id}` and never reached `/users/me`.
Scan all matching routes and keep the most specific via `compareSpecificity()`
(a literal segment beats a `{param}` at the same position), matching how real
routers rank. The listing order of `responder.routes` is unchanged.
…ray types Three correctness/robustness fixes to the OpenAPI response-schema validator, all reachable from `truspec contract` / `run --spec`, where the response under test is effectively untrusted in a contract-testing scenario. DoS — recursive schemas validated in exponential / non-terminating time: - `oneOf`/`anyOf`/`allOf` whose branches `$ref` back to themselves re-validated the same value subtree once per branch → O(2^depth). A ~20-deep response hung for minutes; deeper never returned (`MAX_DEPTH` bounded depth, not the exponential width of re-validation). - A primitive (or self-referential object) value against a `$ref`-cycle schema recursed through the schema graph with no value progress and never terminated. Rewrote the validator so all recursion flows through a single `collect()` that (a) memoizes results by (value-node, schema-node) identity, keeping violation paths relative and rebasing them per caller, and (b) breaks `(value, schema)` stack cycles via a `visiting` guard. Work is now bounded by the number of distinct (value, schema) pairs — linear. Verified across 120k+ randomized fuzz iterations over 6 seeds: 0 hangs, slowest 2ms. False negative — an array `type` (`type: ["string", "null"]`, the idiomatic OpenAPI 3.1 / JSON Schema way to express nullability) was coerced to `undefined` and silently accepted ANY value. A non-null value must now satisfy at least one listed type, and the null guard honors `"null"` in the array. Adds regression tests covering each: exponential oneOf/allOf, primitive and cyclic-value termination, deep-violation reporting, and array-type validation.
Per-cycle record of the break-fix-repeat hardening pass: each finding's repro, root cause, fix, and regression test, plus the surfaces that held (path traversal, prototype pollution, XML injection, malformed HTTP) and the two clean confirmation fuzz cycles.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adversarial QA hardening of the
@truspec/coreengine — a break-fix-repeat pass focused on parser/validator robustness against untrusted bytes (collection files, OpenAPI specs, imported collections, and HTTP responses under contract test) and the correctness of the spec-sync engine.5 bugs found and fixed at the root, each with a regression test. Test suite 206 → 218 passing; typecheck 7/7; build 5/5. Two independent property-fuzz confirmation cycles across 6 seeds (120k+ iterations) came back clean.
Bugs fixed
oneOf/anyOf(depth-18 ≈ 7.7s; depth-20+ never returns)conforms()re-validates the same subtree per branch → O(2^depth)allOf/properties/items(the first memoization fix only coveredoneOf/anyOf)collect()$refschema(value, schema)cyclevisitingguard)/users/me→/users/{id}'s body)respond()returns first regex match in doc order{param})type: ["string","null"]typecoerced toundefined→ "accept anything"The three DoS variants are the headline: all reachable from
truspec contract/run --spec, where the response being validated is exactly the thing under test. The validator is now provably linear — total work is bounded by the number of distinct(value-node, schema-node)pairs.What was attacked and held (no change needed)
Prototype pollution (YAML / Postman+Bruno importers / jsonpath / interpolate), path traversal (web static + API + MCP writes + scaffold — raw,
%2e%2e, double-encoded, null byte, backslash → no leak, no crash), JUnit XML injection, the DNS-rebinding host guard, mock/web HTTP servers vs malformed raw requests, and the live probe (GET/HEAD only).Test plan
pnpm test→ 218 passed (25 files; +12 over baseline, all new tests are regressions for the bugs above)pnpm typecheck→ 7/7 ·pnpm build→ 5/5Full per-cycle findings, repros, and residual-risk notes are in
QA_LOG.md.Known limitations (documented in QA_LOG.md, not changed here)
additionalProperties-as-schema is unvalidated (documented subset gap); the< 6 charsecret-mask threshold;MAX_DEPTH=100still caps very deep non-cyclic recursion (now just a backstop).