HIGH symlink attackflaskhmac signatures

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.

Frequently Asked Questions

Why does HMAC alone not prevent symlink attacks in Flask?
HMAC ensures the integrity of the data used to compute the signature, such as a file path or identifier, but it does not prevent an attacker from replacing the underlying resource with a symlink. If the application resolves the path after verification, the symlink can redirect access to an unintended location. Protection requires canonicalizing paths and confining resolved locations to a strict base directory, rather than relying on the signature alone.
What additional measures complement HMAC signatures to mitigate symlink risks in Flask?
Use server-side mappings for identifiers, resolve paths with os.path.realpath, enforce strict base directory checks, avoid passing client-supplied paths directly to filesystem operations, and ensure that file operations occur within a confined directory tree. These steps ensure that even if a symlink is planted, the application will not follow it to sensitive locations.