HIGH cross site request forgeryflaskapi keys

Cross Site Request Forgery in Flask with Api Keys

Cross Site Request Forgery in Flask with Api Keys

Cross Site Request Forgery (CSRF) in Flask becomes nuanced when endpoints rely on API keys for identification. API keys are typically passed in HTTP headers or query parameters and are intended to identify the calling service or application. Because keys are not designed to enforce user intent, they do not protect against requests that a user’s browser automatically sends, such as image tags or form submissions. If an API key is accepted from any request without verifying that the request originated from an expected source, a CSRF vector can exist despite authentication via key.

Consider a Flask endpoint that processes a money transfer using an API key passed in a header:

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/transfer", methods=["POST"])
def transfer():
    api_key = request.headers.get("X-API-Key")
    if not validate_key(api_key):
        return jsonify({"error": "forbidden"}), 403
    # process transfer using request.json["amount"] and request.json["to"]
    return jsonify({"status": "ok"})

def validate_key(key):
    # simplistic key check for illustration
    return key == "known_key_123"

An attacker can craft a page that sends a POST request to /transfer with a valid API key included. If the victim’s browser automatically includes cookies (e.g., session cookies) and the server also uses cookie-based sessions, the request may be authorized by both the cookie and the API key. The API key alone does not prevent CSRF because it does not bind the request to the user’s intent. In this scenario, the key identifies the client application but does not confirm that the user interacted knowingly with the action. Additionally, if the API key is leaked via logs, browser history, or Referer headers, the attack surface expands because the key can be reused across origins.

CSRF against API keys is also relevant when keys are passed as query parameters. Query parameters can leak through browser logs, proxy logs, and Referer headers, making them easier to steal and reuse in forged requests. Even if the endpoint requires a key, an unauthorized site can embed an image or script that triggers state-changing operations on behalf of a user or system that possesses the key. This is particularly risky for operations that change permissions or invoke financial actions. The key identifies the caller but does not provide a per-request nonce or origin check, which is why CSRF protections must be considered separately.

Flask’s built-in tools do not provide automatic CSRF protection for API-style endpoints. Developers must explicitly enforce origin checks or use anti-CSRF tokens when the threat model includes browser-based forgery. Relying solely on API keys can give a false sense of security, because keys are static credentials and do not rotate per session or per request. Attack patterns like tricking a user’s browser into sending forged requests remain viable when origin validation and CSRF tokens are absent.

Api Keys-Specific Remediation in Flask

Remediation for CSRF when using API keys focuses on ensuring that requests are intentionally initiated by the expected client and origin. Because API keys alone do not bind requests to a user session or browser context, you should combine keys with additional controls such as strict referrer checks, custom request headers, and anti-CSRF tokens for browser-originated actions.

One approach is to require a custom header that cannot be set by simple HTML forms, reducing the risk of CSRF from img or form tags. For example, enforce that state-changing endpoints only accept requests with a header like X-Requested-With or a custom token:

from flask import Flask, request, jsonify, abort

app = Flask(__name__)

@app.route("/transfer", methods=["POST"])
def transfer():
    api_key = request.headers.get("X-API-Key")
    if not validate_key(api_key):
        abort(403)
    if request.headers.get("X-Requested-With") != "XMLHttpRequest":
        abort(403)
    # process transfer
    return jsonify({"status": "ok"})

def validate_key(key):
    return key == "known_key_123"

This pattern relies on the same-origin policy enforced by browsers for JavaScript requests, making it harder for external sites to forge the custom header. However, this method is not foolproof if other vulnerabilities exist, so combining it with other mitigations is recommended.

For browser-based clients that need to perform authenticated actions, use anti-CSRF tokens alongside API keys. Generate a per-session token, store it server-side or in a secure cookie, and require it in forms or headers:

import secrets
from flask import Flask, request, session, jsonify, abort

app = Flask(__name__)
app.secret_key = "super-secret"

@app.route("/get_csrf_token")
def get_csrf_token():
    if "api_key" not in request.args:
        abort(400)
    # validate api_key omitted for brevity
    token = secrets.token_urlsafe(32)
    session["csrf_token"] = token
    return jsonify({"csrf_token": token})

@app.route("/action", methods=["POST"])
def action():
    api_key = request.headers.get("X-API-Key")
    if not validate_key(api_key):
        abort(403)
    token = request.form.get("csrf_token")
    if not token or token != session.get("csrf_token"):
        abort(403)
    # perform action
    return jsonify({"status": "ok"})

def validate_key(key):
    return key == "known_key_123"

When building services consumed by other applications, prefer the double-submit cookie pattern: set a random CSRF token in a cookie and require the same value in a header. This does not rely on server-side sessions and works well with stateless API keys:

from flask import Flask, request, jsonify, make_response
import secrets

app = Flask(__name__)

@app.route("/transfer", methods=["POST"])
def transfer():
    api_key = request.headers.get("X-API-Key")
    if not validate_key(api_key):
        return jsonify({"error": "forbidden"}), 403
    csrf_cookie = request.cookies.get("csrf_token")
    csrf_header = request.headers.get("X-CSRF-Token")
    if not csrf_cookie or csrf_cookie != csrf_header:
        return jsonify({"error": "invalid csrf"}), 403
    # process transfer
    return jsonify({"status": "ok"})

def validate_key(key):
    return key == "known_key_123"

@app.route("/get_csrf")
def get_csrf():
    response = make_response(jsonify({"message": "set csrf cookie"}))
    response.set_cookie("csrf_token", secrets.token_urlsafe(32), samesite="strict")
    return response

These examples illustrate how to combine API keys with explicit CSRF controls. API keys authenticate and identify; additional measures ensure requests are intentional and tied to a user or origin context.

Frequently Asked Questions

Can API keys alone prevent CSRF in Flask?
No. API keys identify callers but do not bind requests to a user’s browser intent, so they do not prevent CSRF. Use anti-CSRF tokens, custom headers, or double-submit cookie patterns alongside keys.
Should I pass API keys in query parameters to avoid CSRF?
Avoid passing API keys in query parameters. They can leak via logs and Referer headers. Use headers instead, and still apply CSRF protections for browser-initiated requests.