Symlink Attack in Axum with Jwt Tokens
Symlink Attack in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A symlink attack in an Axum service that uses JWT tokens for authorization can occur when file system operations intersect with token-based access control in an unsafe way. This typically happens when an endpoint exposed via Axum accepts user input to construct file paths and uses decoded JWT claims (such as user ID or role) to decide whether the request is allowed, but does not validate path traversal or symlink resolution before interacting with the filesystem.
Consider an Axum handler that serves user profile pictures. The handler decodes a JWT to obtain the user identifier, then builds a filesystem path like ./uploads/{user_id}/avatar.png. If the handler resolves this path naively—using something like tokio::fs::read(&path)—an attacker who can control the uploaded files might place a symlink at a predictable location that points outside the intended directory. When the server later reads ./uploads/{user_id}/avatar.png, the filesystem follows the symlink, potentially allowing reads or writes to arbitrary files that the JWT claims alone did not protect. The JWT token provides identity, but if authorization logic relies only on claims without validating the actual resolved path, the symlink bypasses the intended isolation.
In Axum, this risk is amplified when endpoints combine JWT-based authorization with route parameters or query values used in filesystem operations. For example, an endpoint like /files/{file_id} might decode a JWT to confirm the requester has permission for file_id, then open a file stored under a directory derived from the token’s subject. An attacker who can create or influence a symlink in that storage directory can cause the server to access files the JWT does not authorize, leading to unauthorized data exposure or tampering. The vulnerability is not in JWT verification itself, but in the assumption that claims-based authorization is sufficient without validating the final resolved path on disk, especially when symlinks are involved.
Real-world analogies include insecure deserialization or path traversal, but the specific chain here is: a JWT-authenticated Axum endpoint performs filesystem access based on token claims, and an attacker leverages a symlink to redirect that access. This can map to OWASP API Top 10 controls such as broken object-level authorization and excessive data exposure, and may resemble patterns seen in BOLA/IDOR when authorization depends solely on identifiers without context-aware checks like path canonicalization.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Remediation focuses on ensuring that JWT-based authorization is paired with strict filesystem path validation and canonicalization before any file operation. Do not rely on token claims alone to enforce file access boundaries; always resolve and sanitize paths on the server side.
Below is a concrete Axum example showing safe handling. The handler decodes the JWT, builds a target path, canonicalizes it to eliminate .. and symlinks, and confirms the canonical path resides within an allowed base directory before proceeding.
use axum::{routing::get, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
// other claims as needed
}
async fn read_avatar(
axum::extract::Path(file_name): axum::extract::Path<String>,
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Result<axum::response::Json<Vec<u8>>, (axum::http::StatusCode, String)> {
// Verify JWT (example; integrate with your key management)
let token = params.get("token").ok_or((axum::http::StatusCode::UNAUTHORIZED, "missing token".to_string()))?;
let decoded = decode::<Claims>(token, &DecodingKey::from_secret("secret".as_ref()), &Validation::new(Algorithm::HS256))
.map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "invalid token".to_string()))?;
let user_id = &decoded.claims.sub;
let base_dir = Path::new("./uploads").join(user_id);
let requested_path = base_dir.join(file_name);
// Canonicalize to eliminate symlinks and path traversal
let canonical = requested_path.canonicalize()
.map_err(|_| (axum::http::StatusCode::FORBIDDEN, "invalid path".to_string()))?;
// Ensure the resolved path is still within the allowed base directory
let allowed_base = base_dir.canonicalize()
.map_err(|_| (axum::http::StatusCode::FORBIDDEN, "invalid base".to_string()))?;
if !canonical.starts_with(&allowed_base) {
return Err((axum::http::StatusCode::FORBIDDEN, "path outside allowed directory".to_string()));
}
let data = fs::read(canonical)
.await
.map_err(|_| (axum::http::StatusCode::NOT_FOUND, "file not found".to_string()))?;
Ok(axum::response::Json(data))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/files/:file_name", get(read_avatar));
// run app omitted
}
Key points in this example:
- JWT verification is performed first to establish identity.
- File paths are constructed by joining a controlled base directory (derived from the JWT subject) with the user-supplied filename.
Path::canonicalizeresolves symlinks and removes./..components, ensuring the final path is absolute and normalized.- A strict containment check confirms the canonical path remains under the canonicalized base directory, preventing symlink-based escapes.
Additional operational guidance:
- Validate and sanitize filenames before using them in paths; reject paths containing directory separators or other unexpected characters.
- Run the service with the least-privilege filesystem permissions so that even if a symlink is created, the impact is limited.
- Combine this approach with Axum middleware for consistent authorization and error handling, and monitor for unexpected path resolutions as part of security testing.