Symlink Attack in Fastapi
How Symlink Attack Manifests in Fastapi
Symlink attacks in FastAPI applications typically occur when file operations are performed without proper validation of path traversal attempts. FastAPI developers often use Path objects from the pathlib module or os.path functions to handle file uploads and downloads, creating opportunities for attackers to exploit symbolic links.
A common vulnerability pattern emerges when FastAPI endpoints accept file paths or allow users to specify directories for file operations. Consider this problematic FastAPI endpoint:
from fastapi import FastAPI, UploadFile, File
from pathlib import Path
app = FastAPI()
@app.post("/upload/")
async def upload_file(file: UploadFile = File(...), target_dir: str = "/uploads"):
# Vulnerable: no validation of target_dir
target_path = Path(target_dir) / file.filename
# If target_dir contains a symlink, this could write outside intended directory
with open(target_path, 'wb') as f:
content = await file.read()
f.write(content)
return {"message": "File uploaded successfully"}The vulnerability becomes severe when combined with FastAPI's dependency injection system. An attacker could craft requests that traverse symbolic links to overwrite critical files or access sensitive data outside the intended directory structure.
Another FastAPI-specific manifestation occurs with static file serving. When using FastAPI's StaticFiles middleware:
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")If the static directory contains symlinks or if the directory path is user-controllable, attackers can potentially access files outside the intended static directory.
Fastapi-Specific Detection
Detecting symlink vulnerabilities in FastAPI applications requires both static code analysis and runtime scanning. For static analysis, look for these patterns in your FastAPI codebase:
import re
# Patterns to search for in FastAPI code
VULNERABLE_PATTERNS = [
r"Path\(.*\) /", # Path concatenation without validation
r"os\.path\.join.*upload", # File path joins with user input
r"StaticFiles\(directory=.*\)", # Static file mounts
r"open\(.*\, 'wb'\)", # File writes without validation
r"read_bytes\(\)", # File reading without validation
r"\$ref.*file", # OpenAPI references to file operations
]
# Check if FastAPI app has vulnerable file operations
def scan_fastapi_app_for_symlink_vulns(app):
vulnerabilities = []
# Check for user-controllable path parameters
for route in app.routes:
if hasattr(route, 'endpoint'):
# Inspect endpoint function for dangerous patterns
source = inspect.getsource(route.endpoint)
for pattern in VULNERABLE_PATTERNS:
if re.search(pattern, source):
vulnerabilities.append({
'endpoint': route.path,
'pattern': pattern,
'severity': 'high'
})
return vulnerabilitiesFor runtime detection, middleBrick's API security scanner specifically tests FastAPI applications for symlink vulnerabilities by:
- Scanning for unauthenticated endpoints that accept file paths or directory parameters
- Testing path traversal attempts through symbolic links
- Checking static file mounts for directory traversal vulnerabilities
- Analyzing OpenAPI specifications for risky file operation endpoints
- Verifying proper validation of user-supplied paths
middleBrick's FastAPI-specific checks include testing for common symlink attack vectors like:
# Test cases middleBrick would run
TEST_CASES = [
{"name": "Basic path traversal", "input": "../../etc/passwd"},
{"name": "Symlink following", "input": "../sensitive-data"},
{"name": "Null byte injection", "input": "shell.jsp%00.txt"},
{"name": "Windows path traversal", "input": "..\\..\\windows\\win.ini"},
{"name": "URL encoding", "input": "%2e%2e%2f%2e%2e%2f"},
]Fastapi-Specific Remediation
FastAPI provides several native approaches to prevent symlink attacks. The most effective method is using FastAPI's built-in path validation and sanitization features:
from fastapi import FastAPI, UploadFile, File, HTTPException
from pathlib import Path
from fastapi.responses import JSONResponse
app = FastAPI()
# Define a secure base directory
BASE_DIR = Path(__file__).parent / "secure_uploads"
BASE_DIR.mkdir(exist_ok=True)
# Helper function to validate paths
def validate_path(user_path: str) -> Path:
# Resolve and check if path is within base directory
requested_path = Path(user_path).resolve()
base_path = BASE_DIR.resolve()
# Check if resolved path is within base directory
if not str(requested_path).startswith(str(base_path)):
raise HTTPException(status_code=400, detail="Path traversal attempt detected")
# Check if path is a symlink (even if within base directory)
if requested_path.is_symlink():
raise HTTPException(status_code=400, detail="Symlink access not permitted")
return requested_path
@app.post("/upload-secure/")
async def upload_file_secure(file: UploadFile = File(...), target_dir: str = "uploads"):
# Validate and resolve target directory
target_path = validate_path(target_dir) / file.filename
# Ensure parent directory exists
target_path.parent.mkdir(parents=True, exist_ok=True)
# Write file securely
contents = await file.read()
with open(target_path, 'wb') as f:
f.write(contents)
return {"message": "File uploaded securely", "path": str(target_path)}For static file serving, FastAPI's StaticFiles middleware can be configured securely:
from fastapi.staticfiles import StaticFiles
# Secure static file mounting with path validation
@app.on_event("startup")
def setup_secure_static_files():
static_dirs = ["static", "public"]
for dir_name in static_dirs:
dir_path = Path(dir_name).resolve()
# Verify directory exists and is not a symlink
if not dir_path.exists():
raise RuntimeError(f"Static directory {dir_name} does not exist")
if dir_path.is_symlink():
raise RuntimeError(f"Static directory {dir_name} is a symlink")
# Mount securely
app.mount(f"/{dir_name}", StaticFiles(directory=dir_path), name=dir_name)Additional FastAPI-specific protections include using Pydantic models for path validation:
from pydantic import BaseModel, constr
from fastapi import Path
class FilePath(BaseModel):
path: constr(regex=r'^[a-zA-Z0-9_/-]+\.[a-zA-Z0-9]+$') # Allow only safe characters
@app.post("/download/")
def download_file(file_path: FilePath = Body(...)):
# Pydantic validation ensures path format is safe
safe_path = validate_path(file_path.path)
if not safe_path.exists():
raise HTTPException(status_code=404, detail="File not found")
return JSONResponse(content=safe_path.read_text())