Zip Slip in Axum with Firestore
Zip Slip in Axum with Firestore — how this specific combination creates or exposes the vulnerability
Zip Slip is a path traversal vulnerability that occurs when an archive extraction uses user-supplied paths without proper sanitization. In an Axum application that interacts with Firestore, the risk typically arises around file downloads, backups, or import operations where an archive (for example, a ZIP) is extracted on a backend service before data is written to Firestore. If the application uses the archive entries to construct paths for Firestore document IDs or file paths stored in Firestore fields, an attacker can craft entries like ../../../etc/passwd or use crafted paths to escape intended directories. Even though Firestore itself does not execute filesystem extraction, the vulnerability manifests when Axum code processes uploaded archives, extracts them, and uses the extracted file paths as identifiers or metadata destined for Firestore. This can lead to unintended document writes, overwrites, or exposure of sensitive data when paths are not validated.
For example, an Axum handler might accept a ZIP upload to import user assets into Firestore. If the handler iterates over archive entries and uses each entry’s path directly as a Firestore document ID or a field such as storage_path, an attacker can supply a malicious archive containing paths designed to traverse directories. Because Firestore paths are often derived from these values, an attacker may force creation of documents in sensitive locations (e.g., impersonating other users or elevating privileges). Additionally, if the application later serves files using these Firestore-derived paths without canonicalization, it may expose unintended resources. The attack chain combines Axum’s routing and extraction logic with Firestore’s document model, turning a file import feature into a bypass for authorization and data isolation controls.
Because middleBrick scans test input validation and authorization boundaries, including BOLA/IDOR and property authorization checks across authenticated and unauthenticated surfaces, it can detect whether path traversal risks exist in endpoints that handle archives or file metadata destined for Firestore. The scanner does not fix the issue but highlights the exposure and provides remediation guidance, such as strict path validation and canonicalization before any Firestore interaction.
Firestore-Specific Remediation in Axum — concrete code fixes
To remediate Zip Slip in an Axum application that uses Firestore, enforce strict path validation and canonicalization before using any user-influenced path—especially those derived from archive entries—as document identifiers, collection names, or field values. Do not rely on Firestore’s rules alone to prevent path-based overwrites; validate and constrain paths at the application layer.
Safe archive extraction with path canonicalization
When processing ZIP uploads, use a Rust crate such as zip and ensure each entry’s path is resolved against a safe base directory, rejecting any path that escapes that base. Below is an example Axum handler that extracts a ZIP and writes file metadata to Firestore using safe paths:
use axum::{routing::post, Router};
use firestore::*;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, BufReader};
use std::path::{Path, PathBuf};
use tokio::task;
use zip::ZipArchive;
#[derive(Serialize, Deserialize, Debug)]
struct FileMetadata {
name: String,
path: String,
size: u64,
}
async fn handle_upload(
// Assume multipart/form-data parsing elsewhere provides bytes
bytes: Vec,
) -> Result {
let cursor = std::io::Cursor::new(bytes);
let mut archive = ZipArchive::new(cursor).map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))?;
let base_dir = Path::new("/tmp/uploads");
fs::create_dir_all(base_dir).map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))?;
let outpath = match file.enclosed_name() {
Some(path) => path.join(&path.file_name().ok_or_else(|| (axum::http::StatusCode::BAD_REQUEST, "Invalid file name"))?),
None => return Err((axum::http::StatusCode::BAD_REQUEST, "Invalid entry".into())),
};
// Critical: ensure the normalized path is still within base_dir
let normalized = base_dir.join(outpath).canonicalize().map_err(|_| (axum::http::StatusCode::BAD_REQUEST, "Path traversal attempt"))?;
if !normalized.starts_with(base_dir) {
return Err((axum::http::StatusCode::BAD_REQUEST, "Path escapes base directory".into()));
}
// Ensure parent directories exist
if let Some(parent) = normalized.parent() {
fs::create_dir_all(parent).map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
}
let full_path = normalized.to_string_lossy().to_string();
let metadata = FileMetadata {
name: file.name().to_string(),
path: full_path.clone(),
size: file.size() as u64,
};
// Write to Firestore in a blocking task
task::spawn_blocking(move || {
let client = FirestoreClient::new();
let doc_ref = client.collection("uploads").doc(&metadata.name);
doc_ref.set_obj(&metadata).unwrap();
});
// Write file to disk safely
let mut outfile = fs::File::create(&normalized).map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
io::copy(&mut file, &mut outfile).map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
}
Ok(axum::http::StatusCode::OK)
}
fn main() {
let app = Router::new().route("/upload", post(handle_upload));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(axum::service::into_make_service_with_connect_info::<_, (_,)>(app)).unwrap();
}
Key points in this example:
file.enclosed_name()ensures the entry is a valid path without components like..that could traverse directories.- Joining with a controlled base directory and calling
canonicalize()resolves any.,.., or symlink components, and the subsequentstarts_withcheck prevents escape. - Before using any path-derived value as a Firestore document identifier or field, the application treats it as untrusted input and validates it.
Document ID and field validation
When deriving Firestore document IDs from user-influenced data, avoid directly using raw paths. Instead, sanitize or hash them:
use firestore::*;
use sha2::{Sha256, Digest};
fn safe_document_id(path: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(path.as_bytes());
format!("{:x}", hasher.finalize())
}
// Example usage within Firestore write
let raw_path = "user_uploads/../secrets/token.json"; // potentially malicious
let doc_id = safe_document_id(&raw_path);
let client = FirestoreClient::new();
let doc_ref = client.collection("uploads").doc(&doc_id);
doc_ref.set_obj(&FileMetadata { name: "token.json".into(), path: raw_path.into(), size: 1234 }).unwrap();
By hashing the path, you avoid directory traversal in document IDs while maintaining a deterministic ID if needed. Always validate and encode any user-controlled strings used in Firestore paths, collection names, or document IDs to prevent bypasses in authorization logic.