HIGH open redirectflaskhmac signatures

Open Redirect in Flask with Hmac Signatures

Open Redirect in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability

An Open Redirect in Flask when HMAC signatures are used for parameter validation can occur when a developer verifies the signature but then uses a user-controlled value in a redirect without ensuring the target is same-origin. Consider a Flask route that accepts a next parameter and an HMAC-signed token representing the intended destination:

import hmac
import hashlib
from flask import Flask, request, redirect, abort

app = Flask(__name__)
SECRET = b'super-secret-key'

def verify_token(token, expected):
    return hmac.compare_digest(
        token.encode(),
        expected.encode()
    )

@app.route('/login')
def login():
    next_url = request.args.get('next', '/')
    token = request.args.get('token', '')
    expected_token = hmac.new(SECRET, next_url.encode(), hashlib.sha256).hexdigest()
    if not verify_token(token, expected_token):
        abort(403)
    return redirect(next_url)

In this pattern, an attacker can supply a malicious next URL that passes HMAC verification if the signing process includes the attacker-controlled value. If the server signs exactly what the client sends (including the next URL), the signature validates successfully, and the application performs the redirect to an arbitrary external site. This meets the definition of an open redirect: the application redirects the user to a URL without enforcing that the target is trusted.

An additional risk arises when HMACs are used to protect opaque references (e.g., an ID mapped server-side to a URL) but the mapping lookup does not enforce access control. For example, an attacker could enumerate identifiers and observe whether redirects change, inferring valid references. Even when the HMAC prevents tampering with the token format, the application must still ensure the resolved destination is within an approved set of endpoints. Failure to do so can facilitate phishing lures where users are redirected to attacker-controlled domains after a seemingly valid HMAC check.

It is also important to distinguish between using HMACs to protect integrity versus authorization. A valid HMAC confirms the value was generated by the holder of the secret, but it does not confirm the caller is permitted to reach the target resource. If the redirect logic does not cross-check the resolved destination against an allowlist or session context, an authenticated design may still expose an unauthenticated attack surface through the open redirect.

Hmac Signatures-Specific Remediation in Flask — concrete code fixes

To remediate open redirect risks while preserving HMAC integrity checks, ensure the server does not rely on client-supplied URLs for redirection targets. Instead, use an allowlist mapping and have the HMAC protect a server-side identifier rather than the final URL:

import hmac
import hashlib
from flask import Flask, request, redirect, abort

app = Flask(__name__)
SECRET = b'super-secret-key'

# Server-side mapping of safe destinations
SAFE_URLS = {
    'dashboard': '/app/dashboard',
    'profile': '/app/profile',
    'settings': '/app/settings'
}

def verify_token(token, key, payload):
    expected = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(token.encode(), expected.encode())

@app.route('/login')
def login():
    key = request.args.get('key')  # e.g., 'dashboard'
    token = request.args.get('token', '')
    if key not in SAFE_URLS:
        abort(400)
    payload = f'{key}:{SAFE_URLS[key]}'
    if not verify_token(token, SECRET, payload):
        abort(403)
    return redirect(SAFE_URLS[key])

This approach decouples the redirect target from user input. The client provides only a key identifying a pre-approved destination, and the HMAC covers both the key and the canonical path. An attacker cannot substitute an arbitrary URL even if they forge a valid token because the server never uses client-supplied URLs in the redirect.

When you must accept a next URL (for example, to support third-party integrations), validate the parsed hostname against an allowlist of trusted domains and normalize the path to prevent bypass via dot segments or mixed encodings. Combine this with a short-lived, single-use token to reduce replay risk:

from urllib.parse import urlparse
from datetime import datetime, timedelta
import secrets

TRUSTED_REDIRECT_DOMAINS = {'app.example.com', 'cdn.example.com'}

def is_safe_url(url):
    try:
        parsed = urlparse(url)
        if parsed.scheme not in ('http', 'https'):
            return False
        if parsed.hostname not in TRUSTED_REDIRECT_DOMAINS:
            return False
        # Reject URLs with embedded credentials or suspicious ports
        if parsed.port and parsed.port not in (80, 443):
            return False
        return True
    except Exception:
        return False

@app.route('/redirect')
def redirect_user():
    next_url = request.args.get('next', '/')
    token = request.args.get('token', '')
    # Include a timestamp to prevent replay across sessions
    if not token or '|' not in token:
        abort(400)
    parts = token.split('|')
    if len(parts) != 2:
        abort(400)
    payload, issued_at = parts
    if not verify_token(token.split('|')[0], SECRET, payload):
        abort(403)
    try:
        ts = int(issued_at)
    except ValueError:
        abort(400)
    if datetime.utcnow() - datetime.utcfromtimestamp(ts) > timedelta(minutes=5):
        abort(403)
    if not is_safe_url(next_url):
        abort(403)
    return redirect(next_url)

With these measures, you maintain the integrity guarantees of HMAC signatures while ensuring the runtime behavior does not expose an open redirect. The server controls which destinations are reachable, and the signature binds the intended action to a tightly scoped, short-lived context.

Frequently Asked Questions

Can an HMAC always prevent open redirects if I sign the next URL?
No. Signing the next URL with HMAC ensures integrity but does not prevent open redirects if the application uses the signed value directly in a redirect. An attacker can supply a malicious URL that validates successfully and then redirect users to arbitrary domains. Use an allowlist of server-side destinations instead of relying on client-provided URLs.
What additional steps should I take when using HMACs for redirect tokens in Flask?
Ensure the token scope is narrow and short-lived, validate the resolved hostname against a strict allowlist, avoid including sensitive data in the payload that could leak via logs, and couple the HMAC check with session context where applicable. Prefer opaque keys mapped server-side to safe paths rather than sending full URLs to the client.