Zip Slip in Fastapi with Jwt Tokens
Zip Slip in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Zip Slip is a path traversal vulnerability that occurs when an application constructs file paths using user-supplied input without proper validation. In Fastapi applications that use JWT tokens for authentication, the combination of untrusted input in file operations and JWT-based session handling can expose or amplify the risk. A common scenario is an endpoint that receives a JWT access token, validates it, and then uses claims from the token—such as a username or sub—to build dynamic file paths, for example to serve user-specific resources like exported reports or uploaded documents.
If the endpoint does not sanitize path segments and directly concatenates token-derived values (e.g., username) with a base directory, an attacker can supply a malicious filename such as ../../../etc/passwd. When the server joins this filename with the base path, directory traversal occurs, potentially allowing read access to sensitive files. Because the request includes a valid JWT token, the server may trust the request and process it within an authenticated context, making the vulnerability more dangerous: an authenticated session can be leveraged to traverse paths that would otherwise be restricted.
Moreover, Fastapi applications often rely on JWT tokens for stateless authentication, storing user identity and roles in the token payload. If authorization checks rely solely on token validity without verifying that the requested resource belongs to the authenticated subject, an attacker may use Zip Slip to access files associated with other users. For example, an endpoint like /files/{username} that builds a path using the username from the JWT without canonicalization can be abused if the token’s claims are spoofed or if the application does not enforce ownership checks on the filesystem layer.
The interaction between JWT authentication and file path construction becomes critical when endpoints expose file downloads or administrative operations. Even if the JWT is properly signed and contains correct scopes, the server must treat all path inputs as untrusted. Without strict validation—such as resolving paths with os.path.realpath and ensuring they remain within the intended directory—Zip Slip can bypass logical access controls implied by JWT scopes, leading to unauthorized file access.
Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes
To remediate Zip Slip in Fastapi when JWT tokens are used, ensure path construction is isolated from token-derived values and enforce strict path validation. Avoid building filesystem paths directly from JWT claims. Instead, use an allowlist of permitted resources or map token identifiers to safe internal identifiers. Always resolve and normalize paths and verify they remain within the intended base directory.
Example of a vulnerable pattern:
import os
from fastapi import Depends, Fastapi
from jose import jwt, JWTError
app = Fastapi()
SECRET = "secret"
ALGORITHM = "HS256"
BASE_DIR = "/safe/files"
def get_current_user(token: str):
try:
payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid authentication")
return username
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/download/{filename}")
def download_file(filename: str, token: str = Depends(get_current_user)):
# Vulnerable: using token-derived username directly in path
user_dir = os.path.join(BASE_DIR, token) # or username from JWT
filepath = os.path.join(user_dir, filename)
if not filepath.startswith(os.path.realpath(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied")
with open(filepath, "rb") as f:
return f.read()
Issues include using the token or a claim to build a directory path and only performing a prefix check, which can be bypassed with encoded paths or null bytes in some Python versions. The correct approach is to treat the filename as an identifier, map it to a safe location, and avoid concatenating user input into paths derived from JWT claims.
Secure implementation example:
import os
from fastapi import Depends, Fastapi, HTTPException
from jose import jwt
app = Fastapi()
SECRET = "secret"
ALGORITHM = "HS256"
BASE_DIR = "/safe/files"
def get_current_user(token: str):
try:
payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid authentication")
return username
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
def safe_path(user_id: str, filename: str) -> str:
# Map user identity to a safe directory name, avoid direct concatenation
sanitized_user = "user_" + user_id.replace("/", "_").replace("\", "_")
candidate = os.path.normpath(os.path.join("uploads", sanitized_user, filename))
full = os.path.realpath(os.path.join(BASE_DIR, candidate))
if not full.startswith(os.path.realpath(BASE_DIR)):
raise HTTPException(status_code=403, detail="Path traversal detected")
return full
@app.get("/download/{filename}")
def download_file(filename: str, token: str = Depends(get_current_user)):
# Assume get_user_id decodes and returns a safe user identifier
user_id = token # in practice, extract a stable user ID from the token
filepath = safe_path(user_id, filename)
with open(filepath, "rb") as f:
return f.read()
Key practices: resolve paths with os.path.realpath, normalize with os.path.normpath, avoid using raw JWT claims or usernames directly in filesystem paths, and enforce that the final path must start with the real base directory. Treat filenames as opaque identifiers and map them to filesystem-safe names or use a database-backed mapping between tokens and allowed resources.