Symlink Attack in Flask
How Symlink Attack Manifests in Flask
Symlink attacks in Flask applications typically exploit how Flask handles file uploads and static file serving. When an attacker can control filenames or upload paths, they can create symbolic links that point to sensitive files outside the intended upload directory.
The most common Flask-specific scenario involves the send_from_directory function. Consider this vulnerable pattern:
from flask import Flask, request, send_from_directory
app = Flask(__name__)
UPLOAD_FOLDER = '/var/www/uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@app.route('/upload', methods=['POST'])
def upload_file():
file = request.files['file']
filename = file.filename
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return 'File uploaded successfully'
@app.route('/download/<filename>')
def download_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
An attacker could upload a file named ../../../../etc/passwd or create a symlink to /etc/passwd within the upload directory. When another user requests this file through the download endpoint, the symlink attack allows reading sensitive system files.
Another Flask-specific manifestation occurs with blueprint routing. If a blueprint mounts at /api and uses relative paths for file operations, an attacker might exploit path traversal combined with symlinks:
@app.route('/api/<path:filepath>')
def serve_file(filepath):
return send_from_directory('static', filepath)
Here, filepath could contain ../ sequences that, when combined with symlinks in the static directory, bypass intended access controls.
Flask's development server also has unique symlink-related vulnerabilities. The server's automatic reloading can be triggered by symlink changes, potentially causing denial of service or information disclosure if an attacker can manipulate files in watched directories.
Flask-Specific Detection
Detecting symlink attacks in Flask requires both static code analysis and runtime monitoring. For static analysis, look for these Flask-specific patterns:
import ast
import re
def find_symlink_vulnerabilities(code):
tree = ast.parse(code)
issues = []
# Check for send_from_directory usage with user input
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if (isinstance(node.func, ast.Attribute) and
node.func.attr == 'send_from_directory'):
# Check if directory argument is user-controlled
if (isinstance(node.args[0], ast.Name) or
(isinstance(node.args[0], ast.BinOp) and
isinstance(node.args[0].left, ast.Name))):
issues.append({
'type': 'send_from_directory',
'line': node.lineno,
'description': 'Potential symlink attack via send_from_directory'
})
# Check for path.join with user input
for node in ast.walk(tree):
if isinstance(node, ast.Call):
if (isinstance(node.func, ast.Attribute) and
node.func.attr == 'join' and
isinstance(node.func.value, ast.Name) and
node.func.value.id == 'os'):
if isinstance(node.args[1], ast.BinOp):
issues.append({
'type': 'path_join',
'line': node.lineno,
'description': 'Path join with user input may allow symlink attacks'
})
return issues
Runtime detection requires monitoring file operations. Flask middleware can intercept file reads:
from flask import Flask, request, g
import os
def symlink_protection_middleware(app):
def middleware(environ, start_response):
# Check if requested file is a symlink
path = environ.get('PATH_INFO', '')
if path.startswith('/download/'):
filename = path.split('/')[-1]
full_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if os.path.islink(full_path):
# Log and block symlink access
app.logger.warning(f'Symlink attack attempt: {full_path}')
response = 'Forbidden: Symlink access detected'
return Response(response, status=403, mimetype='text/plain')(environ, start_response)
return app(environ, start_response)
return middleware
app.wsgi_app = symlink_protection_middleware(app.wsgi_app)
For comprehensive detection, use automated scanning tools. middleBrick's black-box scanner specifically tests for symlink vulnerabilities by attempting controlled symlink creation and access attempts during its 5-15 second scan:
# Using middleBrick CLI to scan for symlink vulnerabilities
npm install -g middlebrick
middlebrick scan https://your-flask-app.com
The scanner tests common symlink attack patterns including path traversal attempts combined with symlink exploitation, providing a security score and specific findings about symlink-related vulnerabilities.
Flask-Specific Remediation
Remediating symlink attacks in Flask requires defense-in-depth. Start with proper path validation before any file operation:
import os
from pathlib import Path
from flask import abort
def safe_path_join(base_dir, user_path):
# Resolve the user path and check if it stays within base_dir
try:
# Use pathlib for secure path resolution
base = Path(base_dir).resolve()
target = (base / user_path).resolve()
# Ensure target is within base directory
if base in target.parents or target == base:
return str(target)
else:
raise ValueError('Path traversal detected')
except (ValueError, RuntimeError) as e:
raise ValueError(f'Invalid path: {e}')
@app.route('/download/<path:filename>')
def download_file(filename):
try:
safe_path = safe_path_join(app.config['UPLOAD_FOLDER'], filename)
return send_from_directory(app.config['UPLOAD_FOLDER'], safe_path)
except ValueError:
abort(403)
For file uploads, sanitize filenames and prevent symlink creation:
import re
from werkzeug.utils import secure_filename
def secure_upload(file):
# Get original filename
original_filename = secure_filename(file.filename)
# Generate a safe, unique filename
import uuid
safe_filename = f'{uuid.uuid4().hex}_{original_filename}'
# Save file with safe name
file.save(os.path.join(app.config['UPLOAD_FOLDER'], safe_filename))
return safe_filename
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return 'No file part', 400
file = request.files['file']
if file.filename == '':
return 'No selected file', 400
# Process upload with symlink protection
try:
safe_filename = secure_upload(file)
return f'File uploaded as {safe_filename}', 201
except Exception as e:
app.logger.error(f'Upload error: {e}')
return 'Upload failed', 500
Implement runtime symlink detection in your Flask application:
from flask import after_this_request
def check_for_symlinks(directory):
for root, dirs, files in os.walk(directory):
for name in dirs + files:
path = os.path.join(root, name)
if os.path.islink(path):
# Log and optionally remove malicious symlinks
app.logger.warning(f'Found symlink: {path}')
# os.remove(path) # Uncomment to auto-remove
@app.before_request
def security_check():
# Check upload directory for symlinks before processing requests
if request.endpoint == 'download_file':
check_for_symlinks(app.config['UPLOAD_FOLDER'])
For production deployments, use containerization with read-only filesystems where possible, and implement proper file system permissions to prevent symlink creation in upload directories.
Frequently Asked Questions
How can I test if my Flask app is vulnerable to symlink attacks?
../../../../test.txt. Try accessing it through your download endpoint. If you can read files outside the intended directory, you're vulnerable. Use middleBrick's automated scanning to test this systematically without manual exploitation.