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.