Container Escape in Fastapi
How Container Escape Manifests in Fastapi
Container escape in Fastapi applications typically occurs when attackers can manipulate file paths, environment variables, or system calls to break out of the container's isolation. Fastapi's async nature and Python's dynamic features create specific attack vectors that differ from traditional web frameworks.
One common pattern involves path traversal vulnerabilities in file upload or download endpoints. Consider this Fastapi endpoint:
@app.get("/download/")
async def download_file(filename: str):
with open(filename, 'rb') as f:
return StreamingResponse(f, media_type='application/octet-stream')
An attacker can request /download/../../../../etc/passwd to read files outside the container's intended directory. Fastapi's dependency injection system doesn't automatically sanitize path parameters, making this a frequent issue.
Another Fastapi-specific vector involves improper use of subprocess with user-controlled input:
@app.post("/run/")
async def run_command(command: str = Form(...)):
result = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await result.communicate()
return JSONResponse({'output': stdout.decode()})
This async subprocess execution allows command injection that can mount host volumes or access Docker sockets. The /var/run/docker.sock is particularly dangerous as it provides API access to the Docker daemon, enabling container escape through container management APIs.
Environment variable manipulation is another Fastapi-specific concern. Fastapi applications often read configuration from environment variables:
@app.get("/config/")
async def get_config():
db_url = os.getenv('DATABASE_URL', 'sqlite:///./test.db')
return JSONResponse({'db_url': db_url})
If an attacker can influence these environment variables through container runtime configuration or supply chain attacks, they can redirect database connections to host system services or manipulate application behavior.
Fastapi's background tasks and async workers also introduce container escape risks. Background tasks run in separate contexts and may have different filesystem permissions:
@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
await asyncio.to_thread(process_file, file.filename, await file.read())
return JSONResponse({'status': 'uploaded'})
If process_file performs unsafe operations or if the background task has elevated privileges, attackers can exploit timing windows to access host resources.
Fastapi-Specific Detection
Detecting container escape vulnerabilities in Fastapi requires examining both the application code and runtime configuration. Static analysis tools often miss Fastapi-specific patterns due to its async/await syntax and dependency injection system.
middleBrick's scanning approach identifies Fastapi-specific container escape patterns by analyzing the runtime behavior of API endpoints. The scanner tests for path traversal by attempting to access known sensitive files:
# middleBrick would test these patterns automatically:
/download/../../../../etc/passwd
/download/./%2Fetc%2Fpasswd
/download/C:/Windows/win.ini
The scanner also tests for command injection vulnerabilities by sending payloads that attempt to execute system commands through Fastapi's async subprocess handling:
; cat /etc/passwd #
echo "test" && cat /etc/shadow
echo $(whoami)
For Fastapi applications using background tasks, middleBrick analyzes the async task patterns to identify potential privilege escalation paths. The scanner checks if background tasks have access to Docker sockets or other host resources:
# Testing for Docker socket access
curl -s http://localhost:2375/version
middleBrick's OpenAPI spec analysis is particularly effective for Fastapi applications since Fastapi automatically generates OpenAPI specifications. The scanner cross-references the spec with runtime findings to identify endpoints that accept file paths or execute commands:
{
"paths": {
"/download/{filename}": {
"get": {
"parameters": [
{
"name": "filename",
"in": "path",
"schema": { "type": "string" }
}
]
}
}
}
}
The scanner flags path parameters that could allow directory traversal and checks if file operations use safe path joining methods. It also tests for unsafe consumption patterns where Fastapi endpoints accept arbitrary data that could be executed or interpreted as code.
middleBrick's LLM/AI security checks are relevant for Fastapi applications using AI features. The scanner tests for system prompt leakage and prompt injection vulnerabilities that could be used to manipulate Fastapi's AI-powered features to execute unauthorized commands or access sensitive data.
Fastapi-Specific Remediation
Remediating container escape vulnerabilities in Fastapi requires a defense-in-depth approach combining secure coding practices with proper container configuration. Fastapi's async nature requires specific considerations for security.
For path traversal vulnerabilities, use Fastapi's path validation and safe path joining:
from fastapi import FastAPI, HTTPException
from pathlib import Path
import aiofiles
app = FastAPI()
BASE_DIR = Path("/app/data")
@app.get("/download/")
async def download_file(filename: str):
# Validate and sanitize the path
requested_path = Path(filename).resolve()
base_path = BASE_DIR.resolve()
if not requested_path.is_relative_to(base_path):
raise HTTPException(status_code=400, detail="Invalid file path")
if not requested_path.exists():
raise HTTPException(status_code=404, detail="File not found")
async with aiofiles.open(requested_path, 'rb') as f:
return StreamingResponse(await f.read(), media_type='application/octet-stream')
This approach uses pathlib.Path.resolve() to get the absolute path and is_relative_to() to ensure the requested file stays within the base directory. Fastapi's type hints help document the expected input format.
For command injection prevention, avoid subprocess entirely or use strict whitelisting:
from fastapi import FastAPI, HTTPException
import shlex
app = FastAPI()
ALLOWED_COMMANDS = {
'ls': ['/app/data'],
'cat': ['/app/data']
}
@app.post("/run/")
async def run_command(command: str = Form(...)):
parts = shlex.split(command)
cmd = parts[0]
if cmd not in ALLOWED_COMMANDS:
raise HTTPException(status_code=400, detail="Command not allowed")
# Additional validation for arguments
if len(parts) > 1:
raise HTTPException(status_code=400, detail="Arguments not allowed")
# Use a safe execution method
try:
result = await asyncio.create_subprocess_exec(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await result.communicate()
return JSONResponse({'output': stdout.decode()})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
This implementation uses shlex.split() for proper command parsing and validates against a whitelist of allowed commands. The asyncio.create_subprocess_exec() method is safer than create_subprocess_shell() as it doesn't invoke a shell.
For Fastapi applications using background tasks, implement proper isolation and input validation:
from fastapi import BackgroundTasks
from pydantic import BaseModel
from typing import Any
class FileUpload(BaseModel):
filename: str
content: bytes
@app.post("/upload/")
async def upload_file(
file: UploadFile = File(...),
background_tasks: BackgroundTasks = BackgroundTasks()
):
content = await file.read()
background_tasks.add_task(
process_file_safely,
file.filename,
content
)
return JSONResponse({'status': 'processing'})
async def process_file_safely(filename: str, content: bytes):
# Validate filename
safe_filename = filename.replace('..', '').replace('/', '_')
safe_path = BASE_DIR / safe_filename
# Write file safely
async with aiofiles.open(safe_path, 'wb') as f:
await f.write(content)
This pattern uses Pydantic models for input validation and ensures background tasks operate on sanitized data. The BackgroundTasks dependency injection makes it clear which endpoints use async processing.
Container runtime configuration is equally important. Use read-only filesystems where possible, drop unnecessary capabilities, and avoid running as root:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Create non-root user
RUN addgroup --system appgroup && adduser --system --group appgroup appuser
USER appuser
# Create data directory with proper permissions
RUN mkdir -p /app/data && chown appuser:appgroup /app/data
# Use read-only filesystem except for data
VOLUME /app/data
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Combine these code-level fixes with proper container security contexts in your orchestration configuration to prevent container escape even if application vulnerabilities exist.