v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
HIGH AI PREVALENCE: HIGH auth.java.flow.ssrf

Untrusted request input flows into the URL of an outbound HTTP request.

Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.

Why this matters

Because the destination is attacker-controlled, this is a Server-Side Request Forgery (CWE-918): an attacker can point the request at internal services behind your firewall, or at the cloud metadata endpoint (http://169.254.169.254/...) to steal IAM/instance credentials and pivot deeper into your infrastructure.

Never build a request URL straight from a Spring @RequestParam / @RequestBody value or a raw HttpServletRequest.getParameter(...) / getHeader(...) value. Validate the destination against an explicit allow-list of hosts (resolve the URL and check its host against the allow-list, rejecting private/loopback ranges) before issuing the request with RestTemplate, WebClient, OkHttp, or Apache HttpClient.

VULNERABLE
vulnerable.java
package com.example.ssrf;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpEntity;
import javax.servlet.http.HttpServletRequest;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.apache.http.client.methods.HttpGet;
import java.net.URL;

@RestController
class SsrfController {
    private final RestTemplate restTemplate = new RestTemplate();
    private final OkHttpClient http = new OkHttpClient();

    // @RequestParam flows straight into RestTemplate.getForObject.
    @GetMapping("/fetch")
    String fetch(@RequestParam String url) {
        // ruleid: auth.java.flow.ssrf
        return restTemplate.getForObject(url, String.class);
    }

    // @RequestBody flows into RestTemplate.exchange (indirection through a local).
    @PostMapping("/proxy")
    String proxy(@RequestBody String target) {
        String dest = target;
        // ruleid: auth.java.flow.ssrf
        return restTemplate.exchange(dest, HttpMethod.GET, HttpEntity.EMPTY, String.class).getBody();
    }

    // HttpServletRequest.getParameter feeding a new URL (JDK), then opened.
    @GetMapping("/open")
    String open(HttpServletRequest req) throws Exception {
        String u = req.getParameter("u");
        // ruleid: auth.java.flow.ssrf
        URL parsed = new URL(u);
        return parsed.openConnection().getContentType();
    }

    // Servlet header value into an OkHttp Request.Builder.url(...).
    @GetMapping("/okhttp")
    String okhttp(HttpServletRequest req) throws Exception {
        String upstream = req.getHeader("X-Upstream");
        // ruleid: auth.java.flow.ssrf
        Request request = new Request.Builder().url(upstream).build();
        return http.newCall(request).execute().body().string();
    }

    // @RequestParam into Apache HttpClient HttpGet.
    @GetMapping("/apache")
    HttpGet apache(@RequestParam("endpoint") String endpoint) {
        // ruleid: auth.java.flow.ssrf
        return new HttpGet(endpoint);
    }
}
SAFE
safe.java
package com.example.ssrf;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.Set;

@RestController
class SafeSsrfController {
    private final RestTemplate restTemplate = new RestTemplate();
    private static final Set<String> ALLOWED_HOSTS =
            Set.of("api.internal.example.com", "images.example.com");
    private static final Set<String> ALLOWED_URLS =
            Set.of("https://api.internal.example.com/health");

    // Allow-list / host-validation helper that vets the URL and RETURNS the
    // vetted value (or a safe default), clearing the taint at the call site.
    private String validateUrl(String raw) {
        return ALLOWED_HOSTS.contains(URI.create(raw).getHost())
                ? raw
                : "https://api.internal.example.com/health";
    }

    // Safe: a hard-coded constant target — no untrusted input reaches the sink.
    @GetMapping("/health")
    String health() {
        // ok: auth.java.flow.ssrf
        return restTemplate.getForObject("https://api.internal.example.com/health", String.class);
    }

    // Safe: the request value is passed through a host allow-list validator that
    // returns the vetted destination before it reaches the request.
    @GetMapping("/fetch")
    String fetch(@RequestParam String url) {
        String dest = validateUrl(url);
        // ok: auth.java.flow.ssrf
        return restTemplate.getForObject(dest, String.class);
    }

    // Safe: the raw request value is only requested inside an explicit
    // allow-list membership guard, so it is validated before use.
    @GetMapping("/guarded")
    String guarded(HttpServletRequest req) {
        String raw = req.getParameter("url");
        if (ALLOWED_URLS.contains(raw)) {
            // ok: auth.java.flow.ssrf
            return restTemplate.getForObject(raw, String.class);
        }
        return "rejected";
    }
}

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

// oauthlint-disable-next-line auth.java.flow.ssrf -- <reason>

References

https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html ↗https://cwe.mitre.org/data/definitions/918.html ↗