Skip to content

Add fuzz test for adversarial nesting depth#2818

Open
ericproulx wants to merge 1 commit into
masterfrom
test-adversarial-nesting-depth
Open

Add fuzz test for adversarial nesting depth#2818
ericproulx wants to merge 1 commit into
masterfrom
test-adversarial-nesting-depth

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

Adds a fuzz test that hammers Grape's recursive validation paths with randomly-shaped, deeply-nested payloads and asserts the invariant that matters for a resource-exhaustion surface: a request is never answered with a 5xx or an uncaught exception, no matter how it is nested.

This closes the one gap I explicitly flagged as un-covered in the recent validator audit (follow-up to #2814#2817): the runtime recursion on input shape.

Why nesting depth is a surface

Several validation paths recurse on the structure of the incoming params, not on anything Grape controls:

  • AttributesIterator#do_each recurses into nested arrays.
  • Coercion (deep_symbolize_keys) and oneof (deep_dup) walk nested hashes/arrays.

I measured Grape's intrinsic ceiling by driving AttributesIterator#do_each directly with a nested Ruby array: it raises SystemStackError at roughly 2000–3000 levels. SystemStackError is not a StandardError, so nothing in run_validators would catch it.

Why it isn't reachable over HTTP (what the test documents)

The upstream parsers cap depth far below that ceiling, so a real request degrades to a controlled 4xx long before Grape's recursion is stressed:

Vector Cap Result at over-cap depth
Query / form params Rack::Utils.param_depth_limit (32) rejected → clean 4xx
JSON body JSON.parse max_nesting (100) rejected → clean 4xx

I verified end-to-end over raw request bytes that every recursive endpoint (type: Array/Hash scopes, type: JSON, oneof, Set coercion) returns a 4xx at depths of 100, 500, and 2000 — never a 5xx. So the intrinsic ceiling is latent / defense-in-depth, not a live DoS. The fuzz test pins that guarantee in place: if a future change lifted a parser cap or introduced unbounded recursion at a reachable depth, it would fail.

What the test does

  • Defines one API with five endpoints, one per recursive path.
  • Fuzzes 300 requests: random endpoint × random shape (nested arrays / hashes / alternating) × random leaf × random depth in 1..400 (spanning both sides of the parser caps — depths under ~100 genuinely reach and exercise Grape's recursion).
  • Adds 45 anchored cases (depths 100/500/2000 × 5 endpoints × 3 shapes) so the extremes always run regardless of RNG.
  • Asserts every response status is < 500.
  • The generator is seeded from RSpec.configuration.seed, so a failure reproduces deterministically with --seed <n> and the message prints the offending endpoint / shape / depth / leaf.

I confirmed the test has teeth by temporarily injecting a crash into do_each at a shallow, reachable depth — the fuzz loop and anchored cases both caught it.

Notes

  • Pure test addition, no production changes.
  • Runs in well under a second (every deep payload is rejected cheaply by the parsers).
  • Verified stable across seeds 1, 7, 99, 4242, 88888 and the default randomized run. Full suite: 2413 examples, 0 failures. RuboCop clean.

🤖 Generated with Claude Code

Several validation-runtime paths recurse on the shape of incoming params:
AttributesIterator#do_each recurses into nested arrays, and coercion/oneof
walk nested hashes and arrays. A deeply nested payload is a resource-
exhaustion surface.

This fuzzes random shapes (nested arrays, hashes, mixed) and depths across
every recursive path — Array/Hash scopes, type: JSON, oneof, Set coercion —
sending real request bytes, and asserts the request is never answered with a
5xx / uncaught exception. It documents that the upstream parsers
(Rack param_depth_limit, JSON max_nesting) cap depth well below Grape's own
recursion ceiling, so the reachable surface degrades to a controlled 4xx.

The generator is seeded from RSpec's seed, so a failure reproduces with
--seed <n> and prints the offending endpoint/shape/depth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Danger Report

Warnings

  • Unless you're refactoring existing code or improving documentation, please update CHANGELOG.md.

Markdowns

Here's an example of a CHANGELOG.md entry:

* [#2818](https://github.com/ruby-grape/grape/pull/2818): Add fuzz test for adversarial nesting depth - [@ericproulx](https://github.com/ericproulx).

View run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant