Symlink Attack in Actix with Hmac Signatures
Symlink Attack in Actix with Hmac Signatures
A symlink attack in Actix combined with Hmac Signatures occurs when an API endpoint accepts a client-supplied filename or path, uses Hmac Signatures to authorize access to that resource, and resolves the path on the filesystem without ensuring the final target is within an intended directory. If the signature is computed over a path that includes directory traversal sequences (e.g., ../) or symlinks, an attacker can forge a valid signature for a path that points outside the authorized location. Because the server trusts the signature and the path, it may read, write, or serve arbitrary files, effectively bypassing path-based authorization.
Consider an endpoint that serves user-uploaded documents using a signed token. The client sends a filename and an Hmac-SHA256 signature computed over the filename plus a shared secret. If the server uses the signature only to verify integrity of the filename but then joins that filename directly to a base directory without canonicalizing the resolved path, an attacker can provide a filename like ../../etc/passwd. A valid signature for that string allows the server to traverse outside the intended directory and expose sensitive files. This is a BOLA/IDOR pattern where authorization is tied to a signed identifier rather than to the actual resource ownership or path constraints.
In the context of the 12 security checks, this maps to Property Authorization and Input Validation. The signature mechanism must be tied to a canonical, scoped path, and the server must validate that the resolved path remains within the allowed scope. MiddleBrick scans for such mismatches between signed identifiers and actual filesystem access patterns, highlighting cases where signature verification does not prevent directory traversal or symlink resolution.
Hmac Signatures-Specific Remediation in Actix
Remediation centers on ensuring the signed value is bound to a canonical, constrained path and that filesystem operations never follow symlinks outside the intended directory. Always canonicalize and normalize paths on the server side before joining them to the base directory, and verify that the resolved path starts with the allowed base prefix. Do not rely on client-supplied filenames alone; derive paths from a server-side mapping (e.g., a database record) and include a scope or tenant identifier in the signed payload.
Below are concrete Actix examples demonstrating secure handling of Hmac Signatures with path constraints. The first example signs a scoped resource ID and resolves it against a controlled directory, ensuring symlinks cannot escape.
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::path::{Path, PathBuf};
use std::fs;
type HmacSha256 = Hmac;
// Server-side mapping of resource IDs to filesystem paths
fn resolve_path(resource_id: &str) -> Option {
let base = Path::new("/var/app/uploads");
// Use a server-controlled filename to avoid traversal
let filename = format!("{}.bin", resource_id);
let mut path = base.join(filename);
// Canonicalize and ensure the resolved path is within base
if let Ok(canonical) = path.canonicalize() {
if canonical.starts_with(base) {
return Some(canonical);
}
}
None
}
async fn download_file(info: web::Path<(String, String)>) -> impl Responder {
let (resource_id, signature) = info.into_inner();
let secret = b"super-secret-key";
// Recompute expected signature over scoped resource ID
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(resource_id.as_bytes());
let expected = mac.finalize().into_bytes();
let expected_hex = hex::encode(expected);
// Constant-time comparison to avoid timing leaks
if subtle::ConstantTimeEq::ct_eq(&expected_hex.as_bytes(), &signature.as_bytes()).into() {
if let Some(path) = resolve_path(&resource_id) {
match fs::read(&path) {
Ok(bytes) => HttpResponse::Ok().body(bytes),
Err(_) => HttpResponse::InternalServerError().finish(),
}
} else {
HttpResponse::Forbidden().body("Invalid path")
}
} else {
HttpResponse::Forbidden().body("Invalid signature")
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/files/{resource_id}/{signature}", web::get().to(download_file))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
The second example demonstrates how to sign a broader payload that includes a scope and a timestamp, and how to reject paths that resolve outside the permitted directory. It also shows how to avoid symlink-based escapes by canonicalizing on the server side.
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
// Shared secret and allowed base directory
const SECRET: &[u8] = b"api-secret-key";
const ALLOWED_BASE: &str = "/var/app/uploads";
type HmacSha256 = Hmac;
fn build_signed_path(resource_id: &str, timestamp: u64) -> String {
// Server-controlled filename derived from resource_id and timestamp
format!("{}/{}_{}.dat", ALLOWED_BASE, resource_id, timestamp)
}
fn verify_and_resolve(path_candidate: &Path) -> Option {
let base = Path::new(ALLOWED_BASE);
// Canonicalize to resolve any symlinks
let canonical = path_candidate.canonicalize().ok()?;
if canonical.starts_with(base) {
Some(canonical)
} else {
None
}
}
async fn get_resource(info: web::Path<(String, u64, String)>) -> impl Responder {
let (resource_id, timestamp, signature) = info.into_inner();
let path_str = build_signed_path(&resource_id, timestamp);
let path = Path::new(&path_str);
// Verify Hmac over the same scoped payload the client would sign
let mut mac = HmacSha256::new_from_slice(SECRET).expect("HMAC can take key of any size");
mac.update(path_str.as_bytes());
let expected = mac.finalize().into_bytes();
let expected_hex = hex::encode(expected);
if subtle::ConstantTimeEq::ct_eq(&expected_hex.as_bytes(), &signature.as_bytes()).into() {
if let Some(resolved) = verify_and_resolve(path) {
HttpResponse::Ok().body(format!("Resolved to: {:?}", resolved))
} else {
HttpResponse::Forbidden().body("Path outside allowed scope")
}
} else {
HttpResponse::Forbidden().body("Invalid signature")
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/resource/{resource_id}/{timestamp}/{signature}", web::get().to(get_resource))
})
.bind("127.0.0.1:8080")?
.run()
.await
}