Ssrf in Actix
How SSRF Manifests in Actix
Server-Side Request Forgery (SSRF) in Actix applications occurs when user-controlled input is used to construct HTTP requests to internal or external resources without proper validation. In Actix, this typically manifests through route parameters, query strings, or request bodies that contain URLs which are then passed to HTTP clients like reqwest, surf, or Actix's own awc client.
A common Actix SSRF pattern looks like this:
#[get("/proxy/{url}")]
async fn proxy_url(req: HttpRequest, path: web::Path<String>) -> impl Responder {
let url = path.url.clone();
let client = awc::Client::new();
let resp = client.get(url).send().await?;
HttpResponse::build(resp.status()).body(resp.body().await?)
}
This endpoint allows any URL to be proxied, exposing internal services. Attackers can target internal APIs at http://localhost:8080, cloud metadata endpoints like http://169.254.169.254 (AWS), or services behind corporate firewalls.
Actix-specific SSRF vulnerabilities often appear in:
- Webhook handlers that accept callback URLs
- Microservice orchestrators that call other services based on user input
- Configuration fetchers that download files from user-specified locations
- API aggregators that collect data from multiple sources
The danger is amplified in containerized Actix deployments where internal network services are exposed but not publicly accessible, making SSRF the primary attack vector.
Actix-Specific Detection
Detecting SSRF in Actix requires both static analysis of route handlers and dynamic testing of exposed endpoints. middleBrick's Actix-specific detection includes:
Pattern Analysis: Scanning for Actix route handlers that accept URL parameters and use them in HTTP client calls without validation. The scanner identifies patterns like:
#[get("/fetch/{url}")]
async fn fetch_url(path: web::Path<String>) -> impl Responder {
// Vulnerable: direct use of path parameter
let client = reqwest::Client::new();
let resp = client.get(path.url).send().await?;
// ...
}
Runtime Testing: middleBrick actively probes Actix endpoints with SSRF payloads including:
- Internal IP addresses (127.0.0.1, 10.x.x.x, 172.16.x.x, 192.168.x.x)
- Cloud metadata endpoints (169.254.169.254, 169.254.254.169)
- Special protocols (file://, ftp://, gopher://)
- Large payloads to test for blind SSRF
Network Boundary Testing: The scanner attempts connections to both public and private IP ranges to determine if the Actix service can reach internal resources. This reveals whether the application is properly sandboxed.
Protocol Validation: middleBrick checks if Actix endpoints validate the URL scheme, preventing attacks via non-HTTP protocols that could lead to file disclosure or other protocol-specific vulnerabilities.
For Actix applications using awc or reqwest, middleBrick specifically tests the client configuration to ensure no unsafe defaults are enabled.
Actix-Specific Remediation
Fixing SSRF in Actix requires a defense-in-depth approach using Actix's type system and Rust's safety features. Here are Actix-specific remediation patterns:
URL Validation with Actix Extractors:
use actix_web::{get, web, HttpResponse, Responder};
use url::Url;
use std::net::IpAddr;
#[derive(Debug)]
struct ValidatedUrl(Url);
impl actix_web::FromRequest for ValidatedUrl {
type Config = ();
type Error = actix_web::Error;
type Future = Result<Self, Self::Error>;
fn from_request(
req: &HttpRequest,
_payload: &mut actix_http::Payload,
) -> Self::Future {
let path_url = web::Path::::from_request(req, _payload)?.into_inner();
match Url::parse(&path_url) {
Ok(url) => {
// Block private IP ranges
if is_private_ip(&url) {
return Err(actix_web::error::ErrorForbidden("Private IP blocked"));
}
Ok(ValidatedUrl(url))
}
Err(_) => Err(actix_web::error::ErrorBadRequest("Invalid URL"))
}
}
}
fn is_private_ip(url: &Url) -> bool {
if let Some(host) = url.host_str() {
if let Ok(ip) = host.parse::() {
return ip.is_private() || ip.is_loopback();
}
}
false
}
#[get("/safe-proxy/{url}")]
async fn safe_proxy(validated: ValidatedUrl) -> impl Responder {
let client = awc::Client::new();
let resp = client.get(validated.0.as_str()).send().await?;
HttpResponse::build(resp.status()).body(resp.body().await?)
}
Allowlist Approach:
use actix_web::{get, web, HttpResponse, Responder};
use url::Url;
#[get("/fetch-content/{url}")]
async fn fetch_content(path: web::Path<String>) -> impl Responder {
let allowlist = vec![
"https://api.example.com",
"https://data.example.org",
];
match Url::parse(&path.url) {
Ok(url) => {
if allowlist.iter().any(|allowed| url.host_str() == Some(allowed)) {
let client = awc::Client::new();
let resp = client.get(url.as_str()).send().await?;
HttpResponse::build(resp.status()).body(resp.body().await?)
} else {
HttpResponse::BadRequest().body("URL not in allowlist")
}
}
Err(_) => HttpResponse::BadRequest().body("Invalid URL format")
}
}
Network-Level Isolation: Deploy Actix containers with restricted network policies using Docker or Kubernetes:
# docker-compose.yml example
db:
image: postgres
networks:
- internal
api:
build: .
networks:
- internal
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
# No public ports exposed, only internal network access
Using Safe HTTP Clients: Configure awc or reqwest with strict timeout and redirect policies:
let client = awc::ClientBuilder::new()
.max_redirects(2)
.connect_timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(10))
.finish();
Related CWEs: ssrf
| CWE ID | Name | Severity |
|---|---|---|
| CWE-918 | Server-Side Request Forgery (SSRF) | CRITICAL |
| CWE-441 | Unintended Proxy or Intermediary (Confused Deputy) | HIGH |