Symlink Attack in Flask with Hmac Signatures
Symlink Attack in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A symlink attack in Flask that involves HMAC signatures occurs when an application uses HMAC-signed tokens or file paths to verify integrity or origin, but the underlying file operations do not prevent symbolic link races. Consider a scenario where a Flask endpoint accepts a file path, validates an HMAC signature covering the path, and then performs file access based on that path. The signature may be valid, but between validation and file use, an attacker can swap the intended file or directory entry with a symlink pointing to a sensitive resource. Because the HMAC only covers the path string, not the final resolved location, the server follows the symlink and operates on an unintended file.
This becomes an information leak or privilege escalation when the signed path points to a user-controlled destination that resolves via symlink to a file the server would not normally access, such as /etc/passwd or application secrets. In Flask, this can happen if the application constructs file paths from user input, signs them with HMAC to prevent tampering, and then passes the path to utilities like open, send_file, or custom file readers without canonicalizing and confirming the final resolved location. An attacker who can influence the path before the application resolves it may exploit predictable temporary directories or writable locations to plant symlinks that redirect file operations.
Additionally, if the HMAC is used to protect configuration or routing rules that determine which backend service or file backend to use, an attacker who can influence DNS, environment variables, or mount points may be able to cause the application to follow a symlink to a malicious backend. Because the signature is tied to the logical identifier rather than the concrete resolved resource, the server trusts the context created by the symlink. This pattern is common when applications sign logical identifiers for uploaded assets or configuration entries and later resolve them through filesystem operations without verifying that the resolved inode matches the intended target.
The risk is compounded when the application runs with elevated privileges or when the filesystem allows symlink creation by lower-privilege actors. Even with HMAC ensuring path integrity, the trust boundary is effectively shifted from the resolved resource to the path string, and the filesystem becomes the weak link. Tools that scan APIs and endpoints, such as middleBrick, can surface these logic flaws by correlating endpoint behavior with insecure file handling patterns and missing canonicalization checks.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
To remediate symlink risks when using HMAC signatures in Flask, ensure that any file or resource identifier validated via HMAC is canonicalized and verified before use. Do not trust the path or identifier provided by the client after signature verification; instead, resolve it to a real path and confirm it resides within an expected, safe directory. Below are concrete code examples demonstrating secure handling.
First, use os.path.realpath to resolve symlinks and enforce a strict base directory:
import os
import hmac
import hashlib
import secrets
from flask import Flask, request, abort, send_file
app = Flask(__name__)
BASE_DIR = os.path.abspath('/safe/files')
SECRET = secrets.token_bytes(32)
def verify_hmac(data: bytes, signature: str) -> bool:
expected = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/download')
def download():
path = request.args.get('path')
sig = request.args.get('sig')
if path is None or sig is None:
abort(400)
data = path.encode('utf-8')
if not verify_hmac(data, sig):
abort(403)
# Canonicalize and confine to BASE_DIR
resolved = os.path.realpath(os.path.join(BASE_DIR, path.lstrip('/')))
if not resolved.startswith(BASE_DIR):
abort(403)
if not os.path.isfile(resolved):
abort(404)
return send_file(resolved)
This pattern ensures symlinks are resolved before the path is used, and that the final location remains inside the allowed directory. It also avoids trusting the client-supplied path after HMAC validation.
Second, when HMAC is used to sign higher-level identifiers (for example, asset IDs that map to backend storage keys), store the mapping server-side and validate the identifier against a server-controlled lookup before resolving any filesystem or network location:
import hmac
import hashlib
import secrets
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
SECRET = secrets.token_bytes(32)
ASSET_MAP = {
'a1b2c3': '/safe/files/report.pdf',
'd4e5f6': '/safe/files/invoice.csv',
}
def verify_hmac(data: bytes, signature: str) -> bool:
expected = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/asset')
def asset():
asset_id = request.args.get('id')
sig = request.args.get('sig')
if asset_id is None or sig is None:
abort(400)
data = asset_id.encode('utf-8')
if not verify_hmac(data, sig):
abort(403)
# Server-side mapping prevents symlink or path traversal abuse
filepath = ASSET_MAP.get(asset_id)
if filepath is None:
abort(404)
# Still canonicalize and confirm within allowed storage
import os
resolved = os.path.realpath(filepath)
if not resolved.startswith(os.path.abspath('/safe/files')):
abort(403)
return jsonify({'file': resolved})
These approaches decouple trust from client-controlled paths and ensure that HMAC verification is part of a broader safe resolution strategy. They align with secure handling practices and reduce the window for symlink-based attacks while preserving the integrity checks provided by HMAC.