What's the vulnerability?

faraday's build_exclusive_url uses Ruby's URI#merge to combine the connection's base URL with a per-request path. Per RFC 3986, a protocol-relative URL such as //evil.com/path is a network-path reference: when merged, its authority overrides the base URL's authority.

As a result, a user-controlled path passed to get() / post() can redirect the outgoing request to an attacker-chosen host. An application that builds a Faraday connection against a trusted base host but forwards an attacker-influenced path string is exposed to server-side request forgery — the request leaves the configured host entirely.

Root Cause Analysis

# RCA Report: CVE-2026-25765 — Faraday SSRF via Protocol-Relative URL

## Summary

The `faraday` Ruby HTTP client library contained a Server-Side Request Forgery (SSRF) vulnerability in its `build_exclusive_url` method. When a connection was configured with a trusted base URL (e.g., `http://safe.local/api`) and a request path starting with `//` (a protocol-relative or "network-path" reference) was supplied, the `//` prefix incorrectly passed the existing relative-URL guard. Ruby's `URI#+` then interpreted the protocol-relative path as an authority override, replacing the base host with the attacker-specified host. This allowed an attacker-controlled path string to redirect outbound requests to arbitrary hosts, bypassing the intended scope of the Faraday connection.

## Impact

- **Package**: `faraday` (RubyGems)
- **Affected versions**: `1.0.0`–`1.10.4` and `2.0.0`–`2.14.0`
- **Fixed versions**: `1.10.5` and `2.14.1`
- **CWE**: CWE-918 (Server-Side Request Forgery)
- **Risk**: Medium — CVSS 5.8
- **Consequences**: Any application that forwards user-influenced path strings to a Faraday connection without additional validation can be tricked into making requests to internal or attacker-controlled hosts, potentially exposing internal services, cloud metadata endpoints, or allowing request-smuggling attacks.

## Root Cause

The vulnerable code lives in `lib/faraday/connection.rb` inside the `build_exclusive_url` method.

**Vulnerable guard (v2.14.0)**:
```ruby
url = "./#{url}" if url.respond_to?(:start_with?) && !url.start_with?('http://', 'https://', '/', './', '../')
```

Because a protocol-relative URL such as `//evil.com/path` begins with `/`, the guard condition `!url.start_with?('/')` evaluates to `false`, so the URL is **not** prefixed with `./`. When `URI#+` merges the base URL (`http://safe.local`) with `//evil.com/path`, RFC 3986 dictates that the authority component of the merge target overrides the base authority. The resulting URI becomes `http://evil.com/path`, completely leaving the configured base host.

**Fix commit**: [`a6d3a3a0bf59c2ab307d0abd91bc126aef5561bc`](https://github.com/lostisland/faraday/commit/a6d3a3a0bf59c2ab307d0abd91bc126aef5561bc)

**Fixed guard (v2.14.1)**:
```ruby
url = "./#{url}" if url.respond_to?(:start_with?) &&
                    (!url.start_with?('http://', 'https://', '/', './', '../') || url.start_with?('//'))
```

The fix explicitly detects the `//` prefix and prepends `./`, neutralising the authority component. The merged URI then stays scoped to the base host (`http://safe.local///evil.com/path`), preventing host override.

## Reproduction Steps

1. Run `repro/reproduction_steps.sh`.
2. The script installs the vulnerable (`2.14.0`) and fixed (`2.14.1`) gem versions.
3. It starts a local TCP listener on an ephemeral port to stand in for the attacker host.
4. For each version it:
   - Creates a Faraday connection with base URL `http://192.0.2.1` (TEST-NET-1, guaranteed non-routable).
   - Calls `build_exclusive_url('//127.0.0.1:<port>/x')` and records the resulting host.
   - Issues an actual HTTP `GET` with the same protocol-relative path.
   - Counts how many requests the local listener received.
5. Expected evidence:
   - **Vulnerable (2.14.0)**: `built_host` is `127.0.0.1`, the request succeeds with status 404, and the local listener receives ≥1 request.
   - **Fixed (2.14.1)**: `built_host` remains `192.0.2.1`, the request fails with `Connection refused`, and the local listener receives 0 requests.

## Evidence

- `logs/vulnerable.json` — Faraday 2.14.0 runtime results:
  ```json
  {
    "built_url": "http://127.0.0.1:33953/x",
    "built_host": "127.0.0.1",
    "request_status": 404,
    "request_success": true
  }
  ```
- `logs/fixed.json` — Faraday 2.14.1 runtime results:
  ```json
  {
    "built_url": "http://192.0.2.1///127.0.0.1:33953/x",
    "built_host": "192.0.2.1",
    "request_error": "Faraday::ConnectionFailed: Failed to open TCP connection to 192.0.2.1:80 (Connection refused - connect(2) for \"192.0.2.1\" port 80)",
    "request_success": false
  }
  ```
- `repro/runtime_manifest.json` — consolidated verdict:
  ```json
  {
    "verdict": "confirmed",
    "vulnerable_version": "2.14.0",
    "fixed_version": "2.14.1",
    "vulnerable_built_host": "127.0.0.1",
    "fixed_built_host": "192.0.2.1",
    "vulnerable_request_succeeded": true,
    "fixed_request_succeeded": false,
    "vulnerable_listener_requests": 2,
    "fixed_listener_requests": 0
  }
  ```

## Recommendations / Next Steps

- **Upgrade**: Update `faraday` to `>= 1.10.5` or `>= 2.14.1` immediately.
- **Input validation**: Applications accepting user-influenced URL paths should validate that paths do not begin with `//` before passing them to Faraday, as a defense-in-depth measure.
- **Regression testing**: Add unit tests that assert `build_exclusive_url` rejects host override for `//evil.com`, `//evil.com:8080`, `//user:pass@evil.com`, and `///evil.com` (all covered by the upstream spec added in the fix commit).
- **Network segmentation**: Where SSRF risk is high, restrict outbound connectivity from application servers to only required destinations.

## Additional Notes

- **Idempotency**: `repro/reproduction_steps.sh` was executed twice consecutively and produced identical verdicts both times.
- **Edge cases**: The reproduction script uses `192.0.2.1` (RFC 5737 TEST-NET-1) as the base host to guarantee the fixed version fails predictably with `Connection refused` rather than relying on DNS non-resolution, which can be unreliable in environments with wildcard DNS or ISP hijacking.
- **Server log**: The local HTTP listener received **2 requests** during the vulnerable test and **0 requests** during the fixed test, providing direct runtime proof that the protocol-relative path was neutralised by the patch.
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00147
or pruva-verify GHSA-33mh-2634-fwr2
or pruva-verify CVE-2026-25765
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00147/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available