Directory Traversal Attack
A Directory Traversal Attack (also known as Path Traversal) is a security vulnerability that allows an attacker to access files and directories outside the intended directory. This can lead to unauthorized access to sensitive files such as configuration files, password files, or source code.
Why it happens
It occurs when user input is used to construct file paths without proper validation or sanitization, allowing ../ (parent directory) patterns to "traverse" directories.
Vulnerable Example in FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse
import os
app = FastAPI()
BASE_DIR = "/app/files"
@app.get("/read-file")
def read_file(file_name: str):
file_path = os.path.join(BASE_DIR, file_name)
if not os.path.exists(file_path):
return {"error": "File not found"}
return FileResponse(file_path)
Attack Scenario
If an attacker sends:
GET /read-file?file_name=../../etc/passwd
Then file_path becomes:
/app/files/../../etc/passwd -> /etc/passwd
This allows reading arbitrary files on the system.
Secure Version: Prevent Directory Traversal
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path
app = FastAPI()
BASE_DIR = Path("/app/files").resolve()
@app.get("/read-file")
def read_file(file_name: str):
requested_path = (BASE_DIR / file_name).resolve()
# Check if the resolved path starts with the BASE_DIR
if not str(requested_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied")
if not requested_path.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(requested_path)
Key Defenses
Use
Path.resolve()to normalize and resolve symlinks.Always check if the resolved path starts with the base directory.
Do not trust user input for file paths.
Consider using a whitelist of allowed filenames if possible.
Let me know if you'd like to test this securely using dummy files.
Great — here's how to safely test directory traversal in FastAPI with dummy files.
Step-by-Step Demo Setup
Directory Structure:
project/
├── main.py
└── files/
├── public.txt
└── secret.txt
1. Create files
Create a folder files/:
mkdir files
echo "This is a public file" > files/public.txt
echo "This is a secret file" > files/secret.txt
2. main.py (Safe FastAPI app)
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path
app = FastAPI()
BASE_DIR = Path("files").resolve()
@app.get("/read-file")
def read_file(file_name: str):
requested_path = (BASE_DIR / file_name).resolve()
if not str(requested_path).startswith(str(BASE_DIR)):
raise HTTPException(status_code=403, detail="Access denied")
if not requested_path.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(requested_path)
3. Run the server
uvicorn main:app --reload
4. Try a safe request
GET http://localhost:8000/read-file?file_name=public.txt
Response:
This is a public file
5. Try a directory traversal attempt
GET http://localhost:8000/read-file?file_name=../files/secret.txt
Response:
{
"detail": "Access denied"
}
Even though ../files/secret.txt is technically a valid path, it's outside the resolved base and thus blocked.
Extra Test Case
Try accessing an OS-sensitive file if you’re on Linux/macOS:
GET /read-file?file_name=../../etc/passwd
You'll get:
{
"detail": "Access denied"
}
Let me know if you'd like to test with uploads or download APIs too!