Zip Slip in Actix with Cockroachdb
Zip Slip in Actix with Cockroachdb — how this specific combination creates or exposes the vulnerability
Zip Slip is a path traversal vulnerability where an attacker-supplied archive contains files extracted to arbitrary locations outside the intended directory. In an Actix web service that uses CockroachDB as the backend, the risk is not in the database engine itself but in how file paths from uploaded archives are handled before being validated and stored or recorded in CockroachDB.
When an Actix application accepts file uploads, constructs filesystem paths by concatenating a user-provided filename with a base directory, and then persists metadata (such as file name, path, or checksum) into CockroachDB, the combination creates an exploitable chain:
- An attacker uploads a malicious archive containing entries like
../../../etc/passwd. - Actix extracts entries without enforcing path canonicalization or directory confinement.
- Path traversal occurs as files are written outside the upload directory.
- Metadata about the extracted file (e.g., resolved path or user-supplied name) is inserted into CockroachDB, potentially exposing or linking attacker-controlled content to downstream services that read from the database.
Even though CockroachDB does not execute filesystem operations, it can reflect unsafe path handling by storing and later returning paths that enable further attacks, such as authenticated file retrieval or inclusion. The vulnerability is therefore a failure of input validation and path sanitization in the Actix layer, with CockroachDB acting as a datastore that may unintentionally index and serve malicious paths.
For example, an endpoint in Actix that binds a multipart form field file and a JSON field user_id might naively do:
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
use std::path::Path;
async fn upload_file(mut payload: Multipart) -> HttpResponse {
while let Some(item) = payload.next().await {
let mut field = item.unwrap();
let filename = field.content_disposition().get_filename().unwrap_or("unknown").to_string();
let filepath = format!("/uploads/{}", filename); // Unsafe concatenation
let mut f = web::block(|| std::fs::File::create(&filepath)).await.unwrap();
while let Some(chunk) = field.next().await {
let data = chunk.unwrap();
f.write_all(&data).unwrap();
}
// Metadata persisted to CockroachDB using a simplistic path
let conn = pool.get().unwrap();
diesel::sql_query("INSERT INTO files (user_id, path) VALUES ($1, $2)")
.bind::
Here, filepath is built by direct concatenation. An archive with a crafted filename can escape /uploads, and the unsafe path stored in CockroachDB may later be used by other services, amplifying the impact.
Cockroachdb-Specific Remediation in Actix — concrete code fixes
Remediation focuses on strict path sanitization, canonicalization, and ensuring that no user-controlled data directly influences filesystem paths before being stored in CockroachDB. Below are concrete, safe patterns for Actix with CockroachDB integration.
- Use a dedicated upload directory and a database-stored relative path: Store only the filename or a UUID in the database, and resolve the full path server-side.
- Canonicalize and validate paths: Use Rust’s
std::fs::canonicalizeor thepath_cleancrate to remove..components, and ensure the resolved path starts with the intended base directory. - Avoid storing user-supplied paths in CockroachDB: Persist a safe identifier instead of the constructed filesystem path.
Safe Actix upload handler with CockroachDB (Diesel) example:
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
use diesel::prelude::*;
use diesel::sql_types::{Integer, Text};
use std::path::{Path, PathBuf};
// Assume pool is a web::Data<Pool> and uploads_dir is configured
async fn safe_upload_file(
pool: web::Data<Pool>,
mut payload: Multipart,
uploads_dir: web::Data<PathBuf>,
) -> HttpResponse {
while let Some(item) = payload.next().await {
let mut field = match item {
Ok(f) => f,
Err _e => return HttpResponse::BadRequest().body("Invalid field"),
};
let content_disp = field.content_disposition();
let filename = match content_disp.get_filename() {
Some(name) => name,
None => continue,
};
// Use a safe filename: UUID or sanitize input
let safe_name = uuid::Uuid::new_v4().to_string();
let ext = Path::new(filename)
.extension()
.and_t::OsStr::to_str()
.unwrap_or("bin");
let file_stem = Path::new(&safe_name).with_extension(ext);
let filepath = uploads_dir.join(&file_stem);
// Ensure canonical path is within uploads_dir
let canonical_dir = std::fs::canonicalize(&uploads_dir).unwrap_or_else(|_| uploads_dir.to_path_buf());
let canonical_path = std::fs::canonicalize(&filepath).unwrap_or_else(|_| filepath.clone());
if !canonical_path.starts_with(&canonical_dir) {
return HttpResponse::Forbidden().body("Invalid path");
}
// Write file safely
let mut f = match web::block(move || std::fs::File::create(&canonical_path)).await {
Ok(Ok(file)) => file,
_ => return HttpResponse::InternalServerError().finish(),
};
while let Some(chunk) = field.next().await {
let data = match chunk {
Ok(d) => d,
Err _e => break,
};
if let Err(_) = f.write_all(&data) {
return HttpResponse::InternalServerError().finish();
}
}
// Persist metadata using a safe relative identifier
let conn = &pool.get().unwrap();
let insert_result = diesel::insert_into(crate::schema::files::table)
.values((
crate::schema::files::user_id.eq(1),
crate::schema::files::stored_name.eq(safe_name + "." + ext),
crate::schema::files::original_name.eq(filename),
))
.on_conflict_do_nothing()
.execute(conn);
if insert_result.is_err() {
return HttpResponse::InternalServerError().finish();
}
}
HttpResponse::Ok().finish()
}
CockroachDB schema example (safe columns):
-- A minimal safe schema; avoid storing full resolved paths from user input
CREATE TABLE files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INT NOT NULL,
stored_name TEXT NOT NULL, -- safe server-generated name
original_name TEXT, -- for display only
created_at TIMESTAMPTZ DEFAULT now()
);
By combining strict path validation in Actix and storing only safe identifiers in CockroachDB, the Zip Slip attack surface is effectively neutralized while preserving the ability to track uploaded content.