HIGH symlink attackaxumbasic auth

Symlink Attack in Axum with Basic Auth

Symlink Attack in Axum with Basic Auth — how this specific combination creates or exposes the vulnerability

A symlink attack in an Axum service that uses Basic Auth can occur when file-system operations intersect with authenticated routes in an unsafe way. Even though Axum does not directly manage authentication, Basic Auth is typically enforced at the application layer via middleware that inspects headers before allowing access to handlers. If a handler exposes user-controlled paths—such as serving files based on a request parameter or header value—and does not validate that the resolved file lies within an intended directory, an authenticated user may be able to create or follow symbolic links that escape that directory. Because the route is protected by Basic Auth, an attacker may first need to obtain valid credentials through phishing, leakage, or credential reuse, but once authenticated they can leverage the same path traversal techniques to read arbitrary files or overwrite files on the server where the process has write access.

In this combination, the vulnerability arises from trusting input that resolves to a file path, not from the authentication mechanism itself. For example, if an endpoint accepts a filename header or query parameter and passes it to tokio::fs::read or similar operations without canonicalizing and confining it, a symlink placed in a writable location (such as a temporary directory or a directory the user can influence) can point outside the allowed directory. When the handler resolves the path, the operating system follows the symlink, granting access to files or directories the application logic intended to protect. Because the route requires Basic Auth, an attacker may probe for open redirect or SSRF-like behaviors—such as using the credentials to reach an endpoint that proxies internal resources—where the symlink serves as the pivot to escape intended boundaries.

Consider an Axum handler that serves uploaded artifacts and includes a username in the response path without strict validation. If an authenticated user can influence the resolved location via a path component or a header like x-file-name, they might place a symlink at a predictable temporary location pointing to a sensitive file such as /etc/passwd. The Basic Auth requirement means the attacker must first authenticate, but once they have valid credentials they can repeatedly use the endpoint to read the file via the symlink. This illustrates how authentication does not mitigate insecure file handling; it simply shifts the prerequisite from anonymous access to valid credentials, which may be easier to obtain than expected through social engineering or accidental exposure.

Basic Auth-Specific Remediation in Axum — concrete code fixes

To mitigate symlink risks in Axum when using Basic Auth, focus on never trusting user input for filesystem paths, enforcing strict path confinement, and validating resolved paths. Do not concatenate user data into paths without resolving to a canonical absolute path and ensuring it remains within a designated root. Use libraries such as path-clean and explicit prefix checks, and prefer safe APIs that avoid symlink following where possible.

Example: Safe file retrieval in Axum with Basic Auth

use axum::{
    async_trait, extract::Request, extract::FromRequest, http::StatusCode, response::IntoResponse,
    routing::get, Router,
};
use std::path::{Path, PathBuf};
use tokio::fs;

struct AuthenticatedUser {
    username: String,
}

#[async_trait]
impl FromRequest<S> for AuthenticatedUser
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
        let auth_header = req.headers().get("authorization");
        let header_value = auth_header.and_then(|v| v.to_str().ok()).unwrap_or("");
        if let Some(credentials) = header_value.strip_prefix("Basic ") {
            // In production, validate credentials against a secure store
            if credentials == "dXNlcjpwYXNz" { // "user:pass" in base64
                return Ok(AuthenticatedUser { username: "user".to_string() });
            }
        }
        Err((StatusCode::UNAUTHORIZED, "Invalid credentials".to_string()))
    }
}

async fn serve_file(user: AuthenticatedUser, filename: String) -> impl IntoResponse {
    // Define a strict base directory where files are allowed
    let base_dir = Path::new("/srv/public");
    // Normalize user input: remove path components that could traverse
    let clean_name = path_clean::clean(&filename);
    if clean_name.contains('\u{0}') || clean_name.contains("..") {
        return (StatusCode::BAD_REQUEST, "Invalid filename").into_response();
    }
    // Build the target path and canonicalize to resolve symlinks
    let target = base_dir.join(clean_name);
    let canonical = match tokio::fs::canonicalize(&target).await {
        Ok(p) => p,
        Err(_) => return (StatusCode::NOT_FOUND, "File not found").into_response(),
    };
    // Ensure the resolved path remains inside the base directory
    if !canonical.starts_with(base_dir) {
        return (StatusCode::FORBIDDEN, "Access denied").into_response();
    }
    match fs::read(&canonical).await {
        Ok(bytes) => bytes.into_response(),
        Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read file").into_response(),
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/files/:filename", get(|user: AuthenticatedUser, filename: String| async move {
            serve_file(user, filename).await
        }))
        .layer(axum::middleware::from_fn(|req, next| async move {
            // Example middleware to ensure authentication is required for all routes
            let _user = AuthenticatedUser::from_request(req, &()).await?;
            Ok(next.run(req).await)
        }));

    axum::Server::bind("0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Key practices to prevent symlink abuse with Basic Auth

  • Never allow user input to directly name filesystem paths; use a mapping from allowed identifiers to stored filenames.
  • Call canonicalize on resolved paths and verify they start with the intended base directory before any I/O.
  • Avoid following symlinks when serving files; prefer opening by descriptor or using APIs that do not follow symlinks if your threat model includes writable directories.
  • Enforce least-privilege filesystem permissions for the process so that even if a symlink is created, it cannot read or write sensitive locations.
  • Log and monitor authentication attempts and file-access patterns to detect credential compromise or reconnaissance behavior.

Frequently Asked Questions

Does Basic Auth alone prevent symlink attacks in Axum?
No. Basic Auth controls access to endpoints but does not prevent insecure path handling. If an authenticated endpoint uses user input to build filesystem paths without proper confinement, symlink attacks remain possible.
How can I validate that a resolved path is safe in Axum?
Canonicalize the resolved path with tokio::fs::canonicalize and ensure the resulting absolute path starts with your intended base directory. Reject paths that contain null bytes, .. segments after cleaning, or resolve outside the allowed prefix.