Clickjacking in Flask with Basic Auth
Clickjacking in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side attack that tricks a user into interacting with a hidden or disguised UI element inside an attacker-controlled page. When Flask applications use HTTP Basic Authentication and are embedded inside an iframe on a malicious site, the combination can expose authentication entry points to clickjacking. Many browsers show the browser’s built-in Basic Auth dialog when a protected resource is loaded inside an iframe, and if the application does not enforce restrictions, an attacker can overlay invisible or misleading controls over the embedded login UI, capturing credentials without user awareness.
Basic Auth credentials are sent with every request to the protected endpoint, so if the login flow can be embedded and triggered via an iframe, an attacker can lure a victim to a page that causes an automatic authentication request. Even if the response is framed, browsers may still prompt for credentials within the nested context, and user interaction (such as a disguised button click) can complete the authentication sequence. This becomes especially risky when the Flask app exposes admin or sensitive endpoints that rely solely on Basic Auth without additional anti-CSRF or framing protections.
An attacker does not need to parse or modify the Basic Auth mechanism itself; they only need to embed the protected URL in an iframe and manipulate the user interface to capture credentials. Without Content Security Policy (CSP) frame-ancestors rules, X-Frame-Options, or Frame-Options headers, Flask applications remain vulnerable to being framed. Even with authentication, a victim’s browser may render the protected page inside the attacker’s frame, and user interaction can lead to unintended authentication or state change if additional CSRF protections are absent.
Basic Auth-Specific Remediation in Flask — concrete code fixes
To mitigate clickjacking risks when using HTTP Basic Authentication in Flask, combine server-side protections with security headers and application-level framing rules. Below are concrete, working examples you can apply directly in a Flask app.
1. Set anti-framing headers
Prevent your application from being embedded in iframes by setting the X-Frame-Options header. In production, prefer Content-Security-Policy with a restrictive frame-ancestors directive.
from flask import Flask, request, Response
def make_response(content):
resp = Response(content)
# Mitigates clickjacking in browsers that still respect this header
resp.headers['X-Frame-Options'] = 'DENY'
# Modern alternative: restrict who can embed this page
resp.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
return resp
app = Flask(__name__)
@app.route('/')
def index():
return make_response('Home — this page cannot be framed.')
2. Enforce Basic Auth with a custom decorator and secure headers
The following example shows a Flask route protected by Basic Auth, with framing protections applied to the response. It also avoids common pitfalls such as caching credentials in logs by ensuring the header is set on every response for protected routes.
import base64
from functools import wraps
from flask import Flask, request, Response
app = Flask(__name__)
def check_auth(username, password):
# Replace with secure credential verification (e.g., constant-time compare)
return username == 'admin' and password == 's3cr3t'
def authenticate():
return Response(
'Could not verify your access level.',
401,
{'WWW-Authenticate': 'Basic realm="Login Required"'}
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
resp = f(*args, **kwargs)
# Ensure framing protections are present on authenticated responses
if isinstance(resp, Response):
resp.headers['X-Frame-Options'] = 'DENY'
resp.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
else:
# If returning a tuple (response, status), wrap it
from flask import make_response
new_resp = make_response(resp)
new_resp.headers['X-Frame-Options'] = 'DENY'
new_resp.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
return new_resp
return resp
return decorated
@app.route('/admin')
@requires_auth
def admin():
return 'Admin area — protected from embedding.'
if __name__ == '__main__':
app.run(debug=False)
3. Validate and avoid unsafe consumption in embedded contexts
When integrating with external systems or APIs, ensure that your application does not inadvertently expose authentication handlers inside frames. Use CSP frame-ancestors to restrict embedding to known origins, and avoid including sensitive routes in any client-side embeddable UI. Regularly scan your endpoints with tools that test framing behavior and report on exposed authentication surfaces.