Crlf Injection in Flask with Hmac Signatures
Crlf Injection in Flask with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject carriage return (CR, \r) and line feed (\n) sequences into an HTTP header or header-like value. In Flask, if user-controlled data such as a username, redirect target, or a custom header value is reflected in a response header and the developer uses Hmac Signatures only for request authentication (e.g., signing query parameters or a JSON body) without also validating or sanitizing header values, the signature may still be considered valid while the injected CR/LF changes the header structure. This can lead to header splitting, where additional headers are injected, or HTTP response splitting, which may enable cross-site scripting (XSS) via injected headers like Location or Set-Cookie.
Consider a Flask route that uses Hmac Signatures to verify a webhook or API request. The server verifies the Hmac to ensure the request originates from a trusted source, but then reflects a user-supplied value into a response header without sanitization:
from flask import Flask, request, make_response
import hmac
import hashlib
app = Flask(__name__)
SECRET = b'super-secret-key'
def verify_hmac(data, received_signature):
computed = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, received_signature)
@app.route('/webhook')
def webhook():
data = request.args.get('data', '')
sig = request.args.get('sig', '')
if not verify_hmac(data.encode(), sig):
return 'Invalid signature', 400
# Vulnerable: user-controlled 'next' used in a header
next_url = request.args.get('next', '/')
resp = make_response('OK')
resp.headers['X-Redirect'] = next_url # No sanitization
return resp
An attacker could supply next as example.com\r\nSet-Cookie: session=hijacked. If the header value is reflected without validation, the injected CRLF can create a second header line. Because the Hmac was computed only over the request payload and not over the reflected header values, the server still accepts the request as authenticated, but the response header is split, potentially allowing injected cookies or a crafted Location header for open redirects. This illustrates that Hmac Signatures protect the integrity of the request but do not automatically protect response header construction; explicit output encoding and header value validation are required.
Hmac Signatures-Specific Remediation in Flask — concrete code fixes
To prevent Crlf Injection when using Hmac Signatures in Flask, treat all user-controlled data that may reach response headers as untrusted, even when the request’s Hmac is valid. Apply canonical header value rules: disallow CR and LF characters, and sanitize or encode values before assigning them to headers. Below are concrete remediation patterns and code examples.
1. Reject or sanitize header values
Before assigning a user-controlled value to a header, strip or replace CR and LF characters. For string values that should be opaque (e.g., a redirect path), use strict validation (whitelist) or percent-encoding where appropriate.
import re
from flask import Flask, request, make_response
import hmac
import hashlib
app = Flask(__name__)
SECRET = b'super-secret-key'
def verify_hmac(data, received_signature):
computed = hmac.new(SECRET, data, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, received_signature)
SANITIZE_CRLF_RE = re.compile(r'[\r\n]')
def safe_header_value(value):
# Remove CR/LF to prevent header splitting
return SANITIZE_CRLF_RE.sub('', value)
@app.route('/webhook')
def webhook():
data = request.args.get('data', '')
sig = request.args.get('sig', '')
if not verify_hmac(data.encode(), sig):
return 'Invalid signature', 400
next_url = request.args.get('next', '/')
safe_next = safe_header_value(next_url)
resp = make_response('OK')
resp.headers['X-Redirect'] = safe_next
return resp
2. Use strict allowlists for known-safe values
For paths used in redirects or locations, prefer a strict allowlist approach. Parse the target, ensure it is a relative path or matches an allowed host, and avoid reflecting raw user input into headers.
from urllib.parse import urlparse
ALLOWED_HOSTS = {'app.example.com', 'cdn.example.com'}
def is_safe_url(url):
if not url:
return False
# Relative URLs are generally safe for certain headers; ensure no CRLF
if url.startswith('/'):
return '\r' not in url and '\n' not in url
parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
return False
return '\r' not in url and '\n' not in url
@app.route('/redirect')
def redirect_user():
target = request.args.get('to', '/')
if not is_safe_url(target):
return 'Invalid destination', 400
resp = make_response('Redirecting')
resp.headers['X-Safe-Location'] = target
return resp
3. Encode when reflection is unavoidable
If you must reflect a value into a header context, apply encoding that neutralizes control characters. For non-text header values, base64 encoding can be a safe choice, provided the consumer expects it.
import base64
@app.route('/data')
def data_endpoint():
token = request.args.get('token', '')
# Encode token to avoid injection while preserving content
encoded_token = base64.urlsafe_b64encode(token.encode()).decode()
resp = make_response('Data')
resp.headers['X-Token'] = encoded_token
return resp
These patterns complement Hmac Signatures by ensuring that even authenticated requests cannot lead to malformed headers. They address the root cause—untrusted data in headers—rather than relying solely on request integrity checks.