Server-Side Request Forgery (SSRF)¶
What does this mean ?¶
SSRF occurs when an untrusted party can influence a server's outbound request beyond its intended destination or purpose. Common entry points include URL previews, webhooks, imports and document renderers. The request uses the server's connectivity, which can reach resources unavailable to the original caller.
What can happen ?¶
An affected service may expose internal responses, send authenticated requests to unintended systems or consume outbound resources. A response does not need to be displayed for an unwanted request to matter. Impact depends on network access, credentials and the functionality of the destination.
Recommendation¶
When the business needs a finite set of destinations, accept a short ID and map it to a server-owned URL. Do not accept a complete URL merely to select a known integration. Disable automatic redirects; if redirects are necessary, authorize every destination again.
For an arbitrary-URL feature, use an egress proxy or equivalent network policy that validates the actual connection destination. Cover IPv4 and IPv6, loopback, link-local, private and special-use ranges as appropriate. Validate every resolved address and account for DNS changes between validation and connection. A one-time hostname or IP check in application code does not solve DNS rebinding.
Strip destination-inappropriate credentials and headers. Apply connection and response deadlines, response-size limits and content checks. A trusted hostname can still be compromised, so avoid broadly trusting every subdomain.
Sample Code¶
The examples show a fixed-destination health check, not an arbitrary-URL fetcher. status.example.com is a reserved illustrative hostname that must be replaced by an owned service configured by the operator. The caller supplies only status. Do not forward user headers or credentials. Keep network egress restricted to the actual configured service.
Standard-library urllib, with redirects rejected:
import urllib.request
TARGETS = {"status": "https://status.example.com/health"}
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def health_status(destination: str) -> int:
if destination not in TARGETS:
raise ValueError("Unknown destination")
opener = urllib.request.build_opener(NoRedirect())
with opener.open(TARGETS[destination], timeout=5) as response:
return response.status
HTTP errors, including denied redirects, propagate to the caller for controlled handling. The function does not read or return the response body. Review proxy environment configuration in deployment.
Node.js with built-in fetch:
const targets = new Map([['status', 'https://status.example.com/health']]);
async function healthStatus(destination) {
const url = targets.get(destination);
if (!url) throw new Error('Unknown destination');
const response = await fetch(url, {
redirect: 'error',
signal: AbortSignal.timeout(5000),
headers: { Accept: 'application/json' }
});
const status = response.status;
await response.body?.cancel();
return status;
}
The unsafe alternative is fetch(request.query.url). Do not make the fixed URL replaceable through request parameters.
Modern .NET, using a reused HttpClient configured once in the service:
static readonly HttpClient HealthClient = new(
new HttpClientHandler { AllowAutoRedirect = false })
{
Timeout = TimeSpan.FromSeconds(5)
};
static async Task<int> HealthStatus(string destination)
{
if (destination != "status") throw new ArgumentException("Unknown destination");
using var response = await HealthClient.GetAsync(
"https://status.example.com/health", HttpCompletionOption.ResponseHeadersRead);
int status = (int)response.StatusCode;
if (status >= 300 && status < 400)
throw new HttpRequestException("Redirect not permitted");
return status;
}
Do not substitute WebRequest.Create(userUrl) plus a hostname suffix check.
Java 11+ HttpClient; keep the client as a reusable service field:
static final java.net.http.HttpClient CLIENT = java.net.http.HttpClient.newBuilder()
.followRedirects(java.net.http.HttpClient.Redirect.NEVER)
.connectTimeout(java.time.Duration.ofSeconds(5)).build();
static int healthStatus(String destination) throws Exception {
if (!"status".equals(destination)) throw new IllegalArgumentException("Unknown destination");
var request = java.net.http.HttpRequest.newBuilder(
java.net.URI.create("https://status.example.com/health"))
.timeout(java.time.Duration.ofSeconds(5)).GET().build();
var response = CLIENT.send(request, java.net.http.HttpResponse.BodyHandlers.discarding());
if (response.statusCode() >= 300 && response.statusCode() < 400) {
throw new java.io.IOException("Redirect not permitted");
}
return response.statusCode();
}
The response body is discarded, not returned to the caller. Enforce response transfer limits at the HTTP client or egress layer for your real service.
Regression checks¶
Stub the transport rather than requesting real internal addresses. Assert that unknown IDs are rejected before any request, the configured URL is exact, no user-supplied Authorization header is forwarded, and a redirect is rejected. Test timeouts and ensure response bodies are not exposed. For arbitrary-URL systems, separately exercise the egress policy with a controlled resolver and fixture destinations, including all IPv6 and redirect cases.