Skip to main content

Module validate

Module validate 

Source
Expand description

Pre-canonicalisation argument validation for toolset tool dispatch.

The public entry point is validate_toolset_tool_args. It performs an iterative, depth-bounded, node-count-bounded walk over a serde_json::Value to:

  1. Reject any argument object that contains a JS-runtime-dangerous key (the denylist; see ARGS_KEY_DENYLIST) at ANY depth, including keys inside objects nested within arrays.

  2. Reject payloads that nest deeper than TOOLSET_ARGS_MAX_DEPTH.

  3. Reject payloads whose total node count exceeds TOOLSET_ARGS_MAX_NODES. This closes the O(payload-width) unbounded case that the depth bound alone does not prevent (a flat object with millions of keys passes depth-1 but would queue millions of stack entries before the denylist check runs).

§Why this guard exists

When a JS extension attaches a toJSON method to an argument object, the value that PASSES validation differs from the value DISPATCHED after JSON.stringify (the toJSON hook runs during serialisation). In a serde_json::Value world there is no live toJSON method — a Value is inert data — so the Rust realisation of that invariant is: the exact in-memory Value validated here is the one moved into dispatch with no re-parse. The guard is still necessary because:

  • Our canonical JSON output is consumed DOWNSTREAM by a JS agent runtime.
  • A toJSON key in the serialised JSON output enables the serialisation-hook bypass in the downstream runtime.
  • Dangerous keys (then, __proto__, etc.) enable thenable-hijack and prototype-pollution attacks downstream.

Rejecting these keys at the Rust validation layer prevents our serialised output from carrying them.

§Walk strategy

The walk is ITERATIVE — it uses an explicit heap-allocated work-stack (Vec<(&Value, usize)>) rather than native C-stack recursion. This prevents stack overflow on adversarially-deep payloads.

Note: serde_json’s own parse recursion limit (~128 on most builds) already rejects pathologically-deep JSON before our layer, but a Value constructed in-memory (e.g. in a unit test or by a future refactor) can exceed that limit. The iterative walk is therefore bounded independently of the parse path.

§Mutation-before-guard invariant

The caller MUST pass the FINAL post-injection Value to this function. ALL mutation (chain_id injection, envelope_xdr insertion, any future merge) happens BEFORE this call; no insert/merge/serde round-trip occurs between this function and from_value::<TypedArgs>. This is the “freeze before dispatch” invariant: the validated Value is the dispatched Value.

Constants§

ARGS_KEY_DENYLIST
The denylist of JS-runtime-dangerous object-key names.
TOOLSET_ARGS_MAX_DEPTH
Maximum nesting depth for toolset tool argument payloads.
TOOLSET_ARGS_MAX_NODES
Maximum total node count for toolset tool argument payloads.

Functions§

validate_toolset_tool_args
Validate a toolset tool argument payload before dispatch.