logo

Database

Need

Restriction of file writes to server-controlled names inside a fixed directory

Context

• Usage of Rust 1.75+ for building application services

• Usage of the actix-web crate for handling HTTP requests

• Usage of std::fs for writing files

Description

1. Non compliant code

use actix_web::{post, web, HttpResponse};
use serde::Deserialize;
use std::fs;

#[derive(Deserialize)]
struct UploadQuery {
    filename: String,
}...

The `upload` handler below saves the request body with `fs::write`, using the `filename` query parameter as the destination path. The caller chooses where the server writes, and with which name. A value such as `../../home/app/.ssh/authorized_keys` adds the attacker's SSH key, `/etc/cron.d/job` schedules a command, and a path inside the application directory replaces a template, a configuration file or a script that the service later executes. `fs::write` also truncates existing files, so the same request can destroy data. Writing files from request data is a common source of remote code execution. The same weakness appears in Axum handlers that take the destination from `Path`, `Query` or `Multipart` fields, and with `File::create` or `OpenOptions`.

2. Steps

• Never pass request data directly to `fs::write`, `File::create` or `OpenOptions::open` in Actix Web or Axum handlers.

• Accept only a single file name that matches a strict pattern, or generate the stored name on the server and keep the client name as metadata.

• Join the validated name to a fixed upload directory that the service does not execute or serve as code.

• Open new files with `OpenOptions::create_new(true)` so existing files and symbolic links are never overwritten.

• Limit the request body size and validate the content type of uploaded files.

• Run the service with a filesystem user that can only write to the upload directory.

3. Secure code example

use actix_web::{post, web, HttpResponse};
use serde::Deserialize;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};

const UPLOAD_DIR: &str = "/srv/app/uploads";
...

The corrected handler builds the destination in `upload_path`, which accepts only a single file name made of ASCII letters, digits, `.`, `-` and `_`, up to 100 characters, and rejects names that start with a dot. Path separators, `..` segments, absolute paths and hidden files such as `.htaccess` never pass, and the name is joined to a fixed upload directory. The file is opened with `OpenOptions::create_new(true)`, which fails if a file with that name already exists. An attacker can therefore neither overwrite existing files nor follow a symbolic link planted in the directory, because `create_new` refuses to open an existing path of any kind. A stronger variant is to ignore the client name entirely, store the file under a random identifier generated by the server, and keep the original name only as metadata in the database.