October 25, 2026
SSRF: A Sneaky Way to Bypass URL Filters Using HTTP Redirects
Recently when i was hunting for a vulnerability disclosure program, I figured out a new methodology for SSRF bypass.
By Sumit Raj
4 min read
1. Understand the vulnerable application
Suppose a web application has an endpoint:
GET /fetch?url=https://example.com HTTP/1.1
Host: vulnerable-app.comGET /fetch?url=https://example.com HTTP/1.1
Host: vulnerable-app.comThe application fetches the URL provided in the url parameter.
Internally, its logic might look like this:
import requests
@app.route("/fetch")
def fetch():
url = request.args.get("url")
# Application fetches the supplied URL
response = requests.get(url)
return response.textimport requests
@app.route("/fetch")
def fetch():
url = request.args.get("url")
# Application fetches the supplied URL
response = requests.get(url)
return response.textThe application makes an outbound HTTP request to the supplied URL.
If the application also constructs a shell command using that URL, the vulnerability could involve both:
- SSRF (Server-Side Request Forgery)
- OS command injection
However, SSRF and command injection are separate vulnerabilities. SSRF allows the server to make requests to unintended destinations, while command injection occurs when attacker-controlled input reaches a command interpreter in an unsafe manner.
2. Normal request flow
Assume the application has a firewall or input validation rule that blocks suspicious characters and command patterns.
For example, you submit:
GET /fetch?url=https://example.com/?cmd=$(id)GET /fetch?url=https://example.com/?cmd=$(id)The firewall detects the suspicious payload:
$(id)$(id)The request is blocked.
3. How the redirect technique works
HTTP redirects use response status codes such as:
301 Moved Permanently302 Found307 Temporary Redirect308 Permanent Redirect
A server can respond with a 302 and a Location header that tells the HTTP client where to make its next request.
For example:
HTTP/1.1 302 Found
Location: https://example.org/nextHTTP/1.1 302 Found
Location: https://example.org/nextIf the client follows redirects, it makes another request to the URL specified in the Location header.
This introduces an important distinction:
The URL submitted to the application is not necessarily the URL ultimately requested by its HTTP client.
Example request flow
Suppose you control a test redirect server.
The server receives a request to:
GET /redirect HTTP/1.1
Host: redirect.exampleGET /redirect HTTP/1.1
Host: redirect.exampleIt responds:
HTTP/1.1 302 Found
Location: https://destination.example/resourceHTTP/1.1 302 Found
Location: https://destination.example/resourceThe application initially sees:
https://redirect.example/redirecthttps://redirect.example/redirectBut the HTTP client may subsequently request:
https://destination.example/resourcehttps://destination.example/resourceThe complete flow is:
4. Why this can bypass URL validation
Consider an application that performs validation like this:
url = request.args.get("url")
if "localhost" in url:
return "Blocked", 403
if "127.0.0.1" in url:
return "Blocked", 403
response = requests.get(url)url = request.args.get("url")
if "localhost" in url:
return "Blocked", 403
if "127.0.0.1" in url:
return "Blocked", 403
response = requests.get(url)The validation only checks the original input.
If the application accepts a URL pointing to a redirect server, it may validate:
https://redirect.example/redirecthttps://redirect.example/redirectThen the HTTP client follows the redirect to another destination.
The application might never validate the destination URL.
This is a redirect validation gap.
The security issue is not that redirects are inherently malicious. The issue is that the application trusts a URL before redirect processing and does not revalidate the destination afterward.
5. A safe, local demonstration
Let's reproduce the redirect behavior without command injection or access to internal services.
We'll use two local HTTP servers:
- Server A: the redirect server
- Server B: the destination server
Step 1: Create the destination server
Create destination.py:
from http.server import HTTPServer, BaseHTTPRequestHandler
class DestinationHandler(BaseHTTPRequestHandler):
def do_GET(self):
print("Destination received:", self.path)
self.send_response(200)
self.end_headers()
self.wfile.write(
b"Hello from destination server"
)
server = HTTPServer(
("127.0.0.1", 9002),
DestinationHandler
)
print("Destination server running on port 9002")
server.serve_forever()from http.server import HTTPServer, BaseHTTPRequestHandler
class DestinationHandler(BaseHTTPRequestHandler):
def do_GET(self):
print("Destination received:", self.path)
self.send_response(200)
self.end_headers()
self.wfile.write(
b"Hello from destination server"
)
server = HTTPServer(
("127.0.0.1", 9002),
DestinationHandler
)
print("Destination server running on port 9002")
server.serve_forever()This server simply receives requests and returns a message.
Step 2: Create the redirect server
Create redirect.py:
from http.server import HTTPServer, BaseHTTPRequestHandler
class RedirectHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header(
"Location",
"http://127.0.0.1:9002/demo"
)
self.end_headers()
server = HTTPServer(
("127.0.0.1", 9001),
RedirectHandler
)
print("Redirect server running on port 9001")
server.serve_forever()from http.server import HTTPServer, BaseHTTPRequestHandler
class RedirectHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header(
"Location",
"http://127.0.0.1:9002/demo"
)
self.end_headers()
server = HTTPServer(
("127.0.0.1", 9001),
RedirectHandler
)
print("Redirect server running on port 9001")
server.serve_forever()This server returns a redirect to the local destination.
Step 3: Start both servers
Open two PowerShell terminals.
Terminal 1:
python destination.pypython destination.pyTerminal 2:
python redirect.pypython redirect.pyBoth servers should start.
Step 4: Test the redirect
Run:
curl.exe -i http://127.0.0.1:9001/redirectcurl.exe -i http://127.0.0.1:9001/redirectExpected response:
HTTP/1.0 302 Found
Location: http://127.0.0.1:9002/demoHTTP/1.0 302 Found
Location: http://127.0.0.1:9002/demoNow follow the redirect:
curl.exe -i -L http://127.0.0.1:9001/redirectcurl.exe -i -L http://127.0.0.1:9001/redirectExpected result:
HTTP/1.0 302 Found
Location: http://127.0.0.1:9002/demo
HTTP/1.0 200 OK
Hello from destination serverHTTP/1.0 302 Found
Location: http://127.0.0.1:9002/demo
HTTP/1.0 200 OK
Hello from destination serverThe destination server terminal should show:
Destination received: /demoDestination received: /demoThis confirms that the client followed the redirect.
6. How this relates to the command injection example
Your original scenario has an additional component: the destination URL contains a command injection payload.
The conceptual flow is:
Stage
1.User supplies a URL to the vulnerable application
-
Firewall inspects the original URL
-
Application requests the supplied URL
-
Redirect server responds with a new destination
-
HTTP client follows the redirect
-
Application or downstream component processes the resulting request
The critical question is where the command injection actually occurs.
For example, if the application uses a shell command to process the final URL, redirect handling could change which input reaches that command.
But if the application only makes HTTP requests and never invokes a shell, a redirect alone cannot create OS command execution.
Similarly, if the application validates the final destination and safely handles URL parameters, the redirect will not bypass those controls.
Things to test during an authorized assessment
For a client-approved SSRF assessment, I would test the following separately.
Direct URL fetch :- Whether the application performs server-side requests
Redirect following:- Whether the HTTP client follows redirects
Redirect destination validation:- Whether each destination is validated
Redirect count:- Whether excessive redirects are restricted
Protocol restrictions:- Whether only approved schemes are accepted
DNS resolution:- Whether resolved addresses are checked against internal ranges
Command execution evidence:- Whether input actually reaches a shell or command interpreter
Important security checks
A secure implementation should:
- Validate the initial URL.
- Restrict allowed schemes, typically to HTTPS.
- Validate the hostname and resolved IP address.
- Reject loopback, private, link-local, and other prohibited address ranges.
- Revalidate every redirect destination.
- Limit redirect count.
- Avoid shell invocation when processing URLs.
- Use parameterized process execution if external commands are genuinely required.
For SSRF prevention, validating only the hostname string is insufficient. DNS resolution and redirect handling must also be considered.
8. The main takeaway
Your technique exploits a trust-boundary mismatch:
Input validation
โ
Accepted URL
โ
HTTP request
โ
Redirect
โ
New destinationInput validation
โ
Accepted URL
โ
HTTP request
โ
Redirect
โ
New destinationThe firewall may inspect only the first URL, while the HTTP client processes a second URL that was never subjected to equivalent validation.
This is best described as SSRF redirect validation bypass when the security control fails to validate the redirected destination.
If command injection is also present, that is a separate issue that needs evidence showing attacker-controlled data reaches a command interpreter.