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;@RestControllerclass 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;@RestControllerclass 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.