Path Traversal in Axum
How Path Traversal Manifests in Axum
Path traversal vulnerabilities in Axum typically arise when user-controlled input is used to construct file system paths without proper validation. Axum's routing and extraction mechanisms can inadvertently expose this risk when handlers use Path, Query, or raw Request parts to build paths for std::fs operations. A common pattern involves extracting a filename parameter and directly appending it to a base directory.
For example, an endpoint designed to serve user uploads might look like this:
use axum::extract::Path;
use std::fs;
async fn serve_file(Path(filename): Path) -> Result {
let path = format!("./uploads/{}", filename);
fs::File::open(path).map_err(|e| {
(axum::http::StatusCode::NOT_FOUND, e.to_string())
})
}
// Route: axum::Router::new().route("/files/*filename", get(serve_file))
If filename contains ../ sequences (e.g., ../../etc/passwd), the constructed path escapes the intended ./uploads directory. Axum's Path extractor does not decode or sanitize such sequences by default, passing them directly to the handler. This differs from frameworks that automatically normalize paths; Axum gives developers full control, which requires vigilant input handling.
Another vector occurs with query parameters:
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize)]
struct FileParams {
name: String,
}
async fn serve_file_query(Query(params): Query) -> Result {
let path = format!("./data/{}", params.name);
fs::read_to_string(path).map_err(|e| {
(axum::http::StatusCode::BAD_REQUEST, e.to_string())
})
}
Here, params.name is user-controlled and unsanitized. An attacker could submit ?name=../../../etc/shadow to read sensitive files. These patterns are especially dangerous in Axum applications serving static assets, user-generated content, or configuration files where the base directory is assumed to be safe.
Axum-Specific Detection
Detecting path traversal in Axum requires analyzing how user input flows into file system operations. middleBrick identifies these issues through black-box testing by probing parameters with traversal sequences (../, ..\, URL-encoded variants) and monitoring for abnormal responses (e.g., successful file access, error messages revealing internal paths). Since middleBrick scans the unauthenticated attack surface, it tests endpoints without needing credentials, focusing on where input is reflected in file access.
For the Axum examples above, middleBrick would:
- Send requests like
GET /files/%2e%2e%2f%2e%2e%2fetc%2fpasswd(URL-encoded../../etc/passwd) to test thePath-based endpoint. - Probe
GET /serve?name=%2e%2e%2f%2e%2e%2fetc%2fpasswdfor the query-based variant. - Check responses for signs of success (e.g.,
200 OKwith file contents) or error messages that leak path information (e.g.,No such file or directory: ./uploads/../../etc/passwd). - Test encoded and double-encoded bypass attempts (e.g.,
..%252f) to catch incomplete decoding logic.
middleBrick’s scanning includes 12 parallel checks, with path traversal falling under Property Authorization and Input Validation categories. It does not require source code or configuration—only the API URL. When a vulnerability is detected, middleBrick reports it with severity (typically High for file read, Critical for write/delete), location (endpoint and parameter), and remediation guidance tailored to the finding.
For Axum developers, integrating middleBrick into CI/CD via the GitHub Action allows automatic scanning on pull requests. The action can be configured to fail builds if the security score drops below a threshold (e.g., grade C), catching regressions early. Example workflow:
name: API Security Scan
on: [pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run middleBrick scan
uses: middlebrick/github-action@v1
with:
api-url: https://staging.example.com
fail-below: C
This ensures that path traversal and other issues are caught before deployment, leveraging middleBrick’s ability to test the live unauthenticated surface without agents or credentials.
Axum-Specific Remediation
Fixing path traversal in Axum centers on validating and sanitizing user input before it interacts with the file system. Axum does not provide built-in path sanitization, so developers must use Rust’s standard library or trusted crates to neutralize traversal attempts. The key is to ensure the resolved path remains within an intended base directory.
For the Path-based endpoint, use std::path::Path to join and canonicalize paths, then verify the result starts with the base directory:
use axum::extract::Path;
use std::fs;
use std::path::{Path, PathBuf};
async fn serve_file_safe(Path(filename): Path) -> Result {
let base_dir = Path::new("./uploads");
let requested_path = base_dir.join(&filename);
// Normalize path (removes `..` and `.` components)
let normalized_path = match requested_path.canonicalize() {
Ok(p) => p,
Err(_) => return Err((axum::http::StatusCode::BAD_REQUEST, "Invalid path".into())),
};
// Ensure the normalized path is still within base_dir
if !normalized_path.starts_with(base_dir) {
return Err((axum::http::StatusCode::FORBIDDEN, "Path traversal attempt".into()));
}
fs::File::open(normalized_path).map_err(|e| {
(axum::http::StatusCode::NOT_FOUND, e.to_string())
})
}
This approach uses canonicalize() to resolve the absolute path, eliminating ../ and ./ sequences. The starts_with check confirms the file is inside ./uploads. Note that canonicalize() fails if the path does not exist, which is acceptable for read-only endpoints (it prevents information leakage about non-existent paths). For endpoints that create files, check existence before canonicalizing or use fs::metadata separately.
For query-based endpoints, apply the same logic:
use axum::extract::Query;
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Deserialize)]
struct FileParams {
name: String,
}
async fn serve_file_query_safe(Query(params): Query) -> Result {
let base_dir = Path::new("./data");
let requested_path = base_dir.join(¶ms.name);
let normalized_path = match requested_path.canonicalize() {
Ok(p) => p,
Err(_) => return Err((axum::http::StatusCode::BAD_REQUEST, "Invalid path".into())),
};
if !normalized_path.starts_with(base_dir) {
return Err((axum::http::StatusCode::FORBIDDEN, "Path traversal attempt".into()));
}
fs::read_to_string(normalized_path)
.map_err(|e| (axum::http::StatusCode::NOT_FOUND, e.to_string()))
}
Alternative: Use Path::strip_prefix after normalization to avoid starts_with (which can have edge cases with symlinks). However, starts_with is safe when combined with canonicalize() as shown.
For serving static files, consider Axum’s ServeDir from tower-http, which handles traversal internally:
use tower_http::services::ServeDir;
use axum::Router;
let app = Router::new().nest_service("/files", ServeDir::new("./uploads"));
ServeDir securely serves files from a directory, blocking traversal attempts by default. This is often safer than manual implementation for static asset serving.
After fixing, rescan with middleBrick to confirm the vulnerability is resolved. The tool will update the security score and findings, providing validation that the remediation effective against the unauthenticated attack surface.
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |
Frequently Asked Questions
Does Axum automatically prevent path traversal in its routing or extractors?
Path and Query pass user input directly to handlers without decoding or sanitizing path traversal sequences (e.g., ../). Developers must manually validate that constructed file paths remain within an intended base directory using methods like canonicalize() and starts_with checks.