Zip Slip in Axum
How Zip Slip Manifests in Axum
Zip Slip is a directory traversal vulnerability that occurs when extracting ZIP archives without proper path validation. In Axum applications, this typically manifests when handling file uploads or processing archives sent to API endpoints.
The vulnerability allows attackers to craft ZIP files with malicious paths like ../../etc/passwd or ../../../../tmp/pwned. When the server extracts these archives using naive extraction methods, files are written outside the intended directory, potentially overwriting critical system files or placing executables in sensitive locations.
In Axum, common scenarios include:
- File upload endpoints that accept ZIP archives for processing
- API endpoints that extract archives to temporary directories
- Handlers that process user-submitted archives for document conversion or analysis
The core issue stems from using Rust's standard library zip::read::ZipArchive::extract or similar extraction methods without path sanitization. Axum itself doesn't provide extraction utilities, but developers often integrate ZIP handling directly in their route handlers:
use axum::{extract::Multipart, http::StatusCode};
use std::fs;
use zip::read::ZipArchive;
use std::io::Cursor;
async fn upload_zip(mut multipart: Multipart) -> Result<String, StatusCode> {
while let Some(field) = multipart.next_field().await? {
let data = field.bytes().await?.to_vec();
let mut archive = ZipArchive::new(Cursor::new(data))?;
// VULNERABLE: No path validation
let target_dir = "/var/www/uploads/";
archive.extract(target_dir)?; // This is the problem
Ok("Upload successful".to_string())
}
}
The attack vector is straightforward: an attacker crafts a ZIP file containing a file entry with a path like ../../../../../etc/passwd. When extracted, this file overwrites the system's password file, potentially allowing privilege escalation or denial of service.
Axum-Specific Detection
Detecting Zip Slip in Axum applications requires both static analysis and runtime scanning. middleBrick's black-box scanning approach is particularly effective for this vulnerability.
When scanning an Axum API endpoint that accepts file uploads, middleBrick tests for Zip Slip by:
- Sending crafted ZIP archives with traversal paths
- Verifying whether extracted files appear outside the intended directory
- Checking for error handling that might leak system information
- Analyzing the OpenAPI spec for file upload endpoints that lack proper validation
For Axum developers, you can also implement detection in your own code:
use axum::{extract::Multipart, http::StatusCode, Json};
use std::path::Path;
use zip::read::ZipArchive;
use std::io::Cursor;
fn is_suspicious_path(entry_path: &str, base_dir: &Path) -> bool {
let full_path = base_dir.join(entry_path);
// Check if path escapes base directory
if !full_path.starts_with(base_dir) {
return true;
}
// Check for suspicious patterns
let suspicious_patterns = [
"..",
"/../",
"../",
":",
"|",
"&"
];
suspicious_patterns.iter().any(|pattern| entry_path.contains(pattern))
}
async fn secure_upload_zip(mut multipart: Multipart)
-> Result<Json<UploadResult>, StatusCode>
{
while let Some(field) = multipart.next_field().await? {
let data = field.bytes().await?.to_vec();
let mut archive = ZipArchive::new(Cursor::new(data))?;
let target_dir = "/var/www/uploads/";
let target_path = Path::new(target_dir);
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = file.enclosed_name()
.ok_or_else(|| StatusCode::BAD_REQUEST)?;
if is_suspicious_path(outpath.to_str().unwrap_or(""), target_path) {
return Err(StatusCode::BAD_REQUEST);
}
// Safe extraction logic here
}
}
Ok(Json(UploadResult { success: true }))
}
middleBrick's CLI tool makes it easy to scan your Axum endpoints:
# Install middleBrick CLI
npm install -g @middlebrick/cli
# Scan your Axum API endpoint
middlebrick scan http://localhost:3000/api/upload-zip
# With GitHub integration
middlebrick scan https://api.your-app.com/upload --github-repo your-org/your-repo
Axum-Specific Remediation
Remediating Zip Slip in Axum requires a defense-in-depth approach. Here's how to implement secure ZIP handling in your Axum applications:
1. Path Validation
use axum::{extract::Multipart, http::StatusCode};
use std::path::{Path, PathBuf};
use zip::read::ZipArchive;
use std::io::Cursor;
fn sanitize_path(original: &str, base_dir: &Path) -> Result<PathBuf, StatusCode> {
let path = PathBuf::from(original);
let canonical_base = base_dir.canonicalize().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Resolve and check if it's still within base directory
let resolved = canonical_base.join(path).canonicalize().map_err(|_| StatusCode::BAD_REQUEST)?;
if !resolved.starts_with(&canonical_base) {
return Err(StatusCode::BAD_REQUEST);
}
Ok(resolved)
}
async fn safe_upload_zip(mut multipart: Multipart)
-> Result<String, StatusCode>
{
let base_dir = "/var/www/uploads/";
let base_path = Path::new(base_dir);
while let Some(field) = multipart.next_field().await? {
let data = field.bytes().await?.to_vec();
let mut archive = ZipArchive::new(Cursor::new(data))?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = file.enclosed_name()
.ok_or_else(|| StatusCode::BAD_REQUEST)?;
// Validate path
let sanitized_path = sanitize_path(outpath.to_str().unwrap_or(""), base_path)?;
// Create directories if needed
if let Some(parent) = sanitized_path.parent() {
std::fs::create_dir_all(parent)?;
}
// Write file safely
let mut outfile = std::fs::File::create(&sanitized_path)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
Ok("Upload successful".to_string())
}
2. Use Safe Extraction Libraries
Instead of using raw extraction methods, use libraries that handle path validation:
use axum::extract::Multipart;
use camino::{Utf8Path, Utf8PathBuf};
use path_clean::clean;
use std::io::Cursor;
fn safe_extract(zip_data: Vec<u8>, target_dir: &Utf8Path) -> Result<(), String> {
let mut archive = zip::ZipArchive::new(Cursor::new(zip_data)).map_err(|e| e.to_string())?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
let outpath = file.enclosed_name()
.ok_or_else(|| "Invalid path in archive".to_string())?
.to_str().ok_or_else(|| "Invalid UTF-8 path")?
.to_string();
// Clean path and validate
let cleaned = clean(&outpath);
let target_path = Utf8PathBuf::from(target_dir).join(cleaned);
// Ensure path is within target directory
if !target_path.starts_with(target_dir) {
return Err(format!("Path {} escapes target directory", outpath));
}
// Create directories and write file
if let Some(parent) = target_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let mut outfile = std::fs::File::create(&target_path).map_err(|e| e.to_string())?;
std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
}
Ok(())
}
3. Implement Rate Limiting and Size Limits
Combine Zip Slip protection with Axum's built-in middleware:
use axum::{routing::post, Router, Json};
use axum_extra::extract::Payload;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::trace::TraceLayer;
let app = Router::new()
.route("/upload-zip", post(upload_zip_handler))
.layer(RequestBodyLimitLayer::new(10_000_000)) // 10MB limit
.layer(TraceLayer::new_for_http());