java-topology/whitepaper/outreach/grape.md
russell@unturf.com 283a490d6d feat: add 5 outreach docs (15 defects) for batch 4
distlib (3, Python), redmine (3, Ruby), grape (3, Ruby),
solc (3, Solidity/C++), grpc-java (3, Java).
2026-04-13 16:55:04 -04:00

5.4 KiB
Raw Permalink Blame History

Grape — CWE-407 Disclosure Brief

2026-04-13 · Patch available — awaiting upstream merge

Finding

Three O(n²) defects in Grape's parameter validation and route registration. All patched. Two defects fire on every API request that validates multi-value parameters against allowlists or blocklists; one fires at application startup during route registration. All three use Array membership tests where Set or Hash lookups belong.

The Defects

grape-0001 (PATCHED — HIGH): lib/grape/validations/validators/values_validator.rb:36

# In ValuesValidator#check_values? — fires on every request with multi-value params:
param_array = val.nil? ? [nil] : Array.wrap(val)
return param_array.all? { |param| values.include?(param) } unless values.is_a?(Proc)
# values is a plain Array (the allowlist). Array#include? is O(V) per param.
# Total: O(P × V) where P = submitted params, V = allowlist size.

values holds the allowlist defined via params do ... values: [...] end. Every submitted parameter value triggers a linear scan over the entire allowlist. With P=50 submitted values and V=200 allowed values: 10,000 comparisons per request.

grape-0002 (PATCHED — MEDIUM): lib/grape/validations/validators/except_values_validator.rb:19

# In ExceptValuesValidator#validate_param! — fires on every request with except_values:
param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
raise ... if param_array.any? { |param| excepts.include?(param) }
# excepts is a plain Array (the blocklist). Array#include? is O(E) per param.
# Total: O(P × E) where P = submitted params, E = blocklist size.

Companion defect to grape-0001. Same pattern, opposite semantic (blocklist instead of allowlist). Fires on every request containing a multi-value param with except_values configured.

grape-0003 (PATCHED — HIGH): lib/grape/dsl/routing.rb:176

# In DSL::Routing#route — fires at application startup for every route:
endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) }
# endpoints is an Array. endpoints.any? scans all existing endpoints.
# With N routes: 1+2+3+...+N = O(N²/2) total comparisons during boot.

On each call to route(), endpoints.any? scans all previously registered endpoints for duplicates. With N routes, total startup cost grows as N²/2.

Complexity Proof

grape-0001: At P=50 params, V=200 allowed values:

  • Defective: 50 × 200 = 10,000 comparisons per request
  • Fixed: 200 (build set) + 50 (lookups) = 250 operations
  • 40x op reduction per request.

grape-0002: At P=30 params, E=150 excluded values:

  • Defective: 30 × 150 = 4,500 comparisons per request
  • Fixed: 150 (build set) + 30 (lookups) = 180 operations
  • 25x op reduction per request.

grape-0003: At N=500 routes:

  • Defective: 500 × 499 / 2 = 124,750 comparisons during boot
  • Fixed: 500 hash lookups = 500 operations
  • 250x op reduction at startup.

Impact

Grape powers REST APIs for thousands of Ruby applications, from startups to large enterprises. grape-0001 and grape-0002 fire on every API request that validates multi-value parameters. High-traffic APIs with large allowlists (enum validation, multi-select filters, tag systems) pay quadratic cost on every inbound request.

grape-0003 fires at application startup. Large Grape APIs with hundreds of routes experience slow cold-start and reload times proportional to route count squared. Container orchestration systems (Kubernetes, ECS) that frequently restart pods amplify this cost.

The Fix

grape-0001: Convert allowlist to Set before validation:

# Before
return param_array.all? { |param| values.include?(param) }

# After
# CWE-407 fix: build Set once for O(1) lookup.
values_set = values.is_a?(Set) ? values : values.to_set
return param_array.all? { |param| values_set.include?(param) }

grape-0002: Convert blocklist to Set before validation:

# Before
raise ... if param_array.any? { |param| excepts.include?(param) }

# After
# CWE-407 fix: O(1) lookup.
excepts_set = excepts.is_a?(Set) ? excepts : excepts.to_set
raise ... if param_array.any? { |param| excepts_set.include?(param) }

grape-0003: Track endpoint identity in a Hash alongside the Array:

# Before
endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) }

# After
# CWE-407 fix: Hash for O(1) duplicate detection.
key = new_endpoint.identity_key
unless @endpoints_seen.key?(key)
  @endpoints_seen[key] = true
  endpoints << new_endpoint
end

Patch

Fix available: defects/grape/patch/grape-0001-values-validator-set.patch, defects/grape/patch/grape-0002-except-values-validator-set.patch, defects/grape/patch/grape-0003-routing-endpoints-any-set.patch

Three patches across values_validator.rb, except_values_validator.rb, and routing.rb.

grape-0001: 40x speedup at P=50, V=200. grape-0002: 25x speedup at P=30, E=150. grape-0003: 250x speedup at N=500 routes.

What We Ask

A patch is ready for review.

  1. Confirm receipt and assign a tracker reference (ruby-grape/grape).
  2. Assess severity — grape-0001/0002 fire on every API request with multi-value params; grape-0003 fires at application startup.
  3. Coordinate a disclosure date — we are targeting 90 days from first contact.
  4. We will credit the Grape team in the public disclosure. Preferred acknowledgment format welcome.

Contact: see cover email. This brief is confidential until coordinated disclosure.