Symlink Attack in Actix with Basic Auth
Symlink Attack in Actix with Basic Auth — how this specific combination creates or exposes the vulnerability
A symlink attack in Actix with Basic Auth occurs when an authenticated or unauthenticated actor can control file paths used by the server and can place or follow symbolic links to reach files outside the intended directory. In Actix web applications, this typically arises from dynamic file serving or user-controlled download endpoints where a path parameter is concatenated with a base directory without proper canonicalization. If the endpoint also relies on Basic Auth for authorization, the developer may assume that only authenticated users can trigger file access. However, because the attack is about path manipulation, an authenticated user (or an unauthenticated user if the endpoint is exposed) can supply a crafted path that traverses upward via .. or uses a symlink to read arbitrary files, such as application source code or configuration that contains credentials.
Consider an Actix handler that streams a file from a user-supplied path without resolving and restricting it to a safe base directory:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use std::path::PathBuf;
async fn download_file(path: web::Path) -> impl Responder {
let base = "/var/app/uploads";
let full = PathBuf::from(base).join(path.into_inner());
match std::fs::read(&full) {
Ok(bytes) => HttpResponse::Ok().body(bytes),
Err(_) => HttpResponse::NotFound().finish(),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/files/{path:.*}", web::get()._to(download_file))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
If Basic Auth is applied via middleware or guards to this route, the protection is only on the HTTP request, not on the filesystem operation. An authenticated user can request /files/../../../etc/passwd or, more effectively, place a symlink in a location they can control that points to /etc/passwd. Because the server joins the base with the user input, the symlink target is reached, and sensitive data is exfiltrated. The presence of Basic Auth may also mislead developers into thinking the file namespace is bounded, when in fact the path traversal bypasses logical boundaries but not the filesystem checks unless explicitly enforced.
Additionally, if the Actix application exposes an unauthenticated endpoint that later receives Basic Auth protection in a separate deployment, a prior scan might not detect the symlink risk. An attacker can exploit path concatenation and symbolic link resolution regardless of whether credentials are required, especially when authorization logic is applied at a higher layer and does not validate canonical paths. This makes the combination of dynamic path joining and Basic Auth a common misconfiguration that leads to unauthorized file disclosure.
Basic Auth-Specific Remediation in Actix — concrete code fixes
To remediate symlink risks in Actix when using Basic Auth, ensure that user input never directly influences filesystem paths. Instead, normalize and restrict all paths to a single, controlled directory. Use canonicalization to eliminate .. and symlink components before joining with the base. Apply path validation logic before any file operation, and avoid exposing raw filesystem paths to the client.
The following example demonstrates a secure handler that uses Path::canonicalize to resolve the final path and confirm it remains within the allowed base directory. It also includes a simple Basic Auth guard using the actix-web-httpauth crate:
use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use actix_web_httpauth::extractors::basic::BasicAuth; use std::path::{Path, PathBuf}; async fn download_file( path: web::Path, auth: BasicAuth, ) -> impl Responder { // Verify credentials — replace with your own check if auth.user() != "admin" || auth.password() != "secret" { return HttpResponse::Unauthorized().finish(); } let base = "/var/app/uploads"; let requested = Path::new(&path); // Prevent directory traversal and symlink escapes let full = PathBuf::from(base).join(requested); let canonical_base = Path::new(base).canonicalize().unwrap_or_else(|_| Path::new(base).into()); match full.canonicalize() { Ok(canonical) => { if canonical.starts_with(canonical_base) { match std::fs::read(&canonical) { Ok(bytes) => HttpResponse::Ok().body(bytes), Err(_) => HttpResponse::NotFound().finish(), } } else { HttpResponse::Forbidden().body("Path outside allowed directory") } } Err(_) => HttpResponse::BadRequest().finish(), } } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/files/{path:.*}", web::get().to(download_file)) }) .bind("127.0.2.1:8080")? .run() .await } Key remediation practices specific to Basic Auth and path handling:
- Validate and sanitize all user input before using it in filesystem operations; prefer allowlists for filenames.
- Always canonicalize both the base directory and the resolved path to detect symlink components.
- Ensure authorization checks are tied to the operation, not assumed from transport-level authentication like Basic Auth.
- Do not rely on path prefixes alone; use prefix checks after canonicalization to confirm containment within the intended directory.