# REPRO-2026-00147: Faraday: SSRF via protocol-relative URL overriding base authority ## Summary Status: published Severity: medium Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00147 GHSA: GHSA-33mh-2634-fwr2 CVE: CVE-2026-25765 ## Package Name: faraday Ecosystem: rubygems Affected: faraday >= 1.0.0, <= 1.10.4 and >= 2.0.0, <= 2.14.0 Fixed: Unknown ## Root Cause # 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:/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. ## Reproduction Details Reproduced: 2026-05-22T18:01:58.333Z Duration: 615 seconds Tool calls: 128 Turns: 102 Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00147 pruva-verify GHSA-33mh-2634-fwr2 pruva-verify CVE-2026-25765 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00147&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00147/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-33mh-2634-fwr2 - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-25765 - Source: https://github.com/lostisland/faraday ## Artifacts - repro/rca_report.md (analysis, 5851 bytes) - repro/reproduction_steps.sh (reproduction_script, 5412 bytes) - vuln_variant/rca_report.md (analysis, 5471 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 3968 bytes) - bundle/context.json (other, 3062 bytes) - bundle/metadata.json (other, 721 bytes) - bundle/ticket.md (ticket, 3469 bytes) - repro/runtime_manifest.json (other, 312 bytes) - repro/validation_verdict.json (other, 1001 bytes) - vuln_variant/test_edge_cases.rb (other, 1038 bytes) - vuln_variant/test_latest.rb (other, 779 bytes) - vuln_variant/test_vuln_2140.rb (other, 646 bytes) - vuln_variant/root_cause_equivalence.json (other, 1018 bytes) - vuln_variant/patch_analysis.md (documentation, 4547 bytes) - vuln_variant/variant_manifest.json (other, 2517 bytes) - vuln_variant/test_e2e_bypass.rb (other, 982 bytes) - vuln_variant/test_bypass_2141.rb (other, 703 bytes) - vuln_variant/validation_verdict.json (other, 1224 bytes) - vuln_variant/source_identity.json (other, 814 bytes) - vuln_variant/test_bypass.rb (other, 1044 bytes) - logs/fixed.json (other, 347 bytes) - logs/vulnerable.json (other, 204 bytes) - logs/server_requests.log (log, 0 bytes) - logs/vuln_variant/fixed_version.txt (other, 189 bytes) - logs/vuln_variant/variant_evidence.log (log, 2074 bytes) - logs/variant_evidence.log (log, 2074 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00147 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00147/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00147 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev