Skip to main content

csrf-kit

Lightweight Cross-Site Request Forgery (CSRF) protection for Flask, with token generation and verification built on Python's standard library.

Version: 0.1.0 (Alpha)
Requirements: Python 3.10+ and Flask 3.1+

What does it do?

CSRF is an attack in which another website tricks a user's browser into sending an unwanted request to a site where the user is logged in. csrf-kit adds a session-bound token to legitimate requests and rejects requests that do not provide a valid token.

By default, it automatically checks unsafe HTTP methods such as POST, PUT, PATCH, and DELETE. Ordinary GET requests do not require a token. Invalid or missing tokens produce an HTTP 400 Bad Request response.

Installation

Install the package:

python -m pip install csrf-kit

Or, from an extracted source directory containing pyproject.toml:

python -m pip install .

Basic Flask setup

Create app.py:

import os
from flask import Flask, render_template
from csrf_kit import CSRF

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]

csrf = CSRF(app)

@app.get("/")
def home():
    response = app.make_response(render_template("form.html"))
    response.headers["Cache-Control"] = "no-store"
    return response

@app.post("/submit")
def submit():
    return "Form submitted successfully!"

Explanation: Flask uses SECRET_KEY to protect its session cookie; csrf-kit uses this key (unless configured otherwise) to sign tokens. CSRF(app) enables automatic validation for unsafe requests. The /submit route does not need a separate decorator. Set SECRET_KEY to a strong, stable, private value in your environment; do not hardcode it in a public repository.

For local development, set the SECRET_KEY environment variable before starting the application, then run:

flask --app app run

Protect an HTML form

Create templates/form.html:

<!doctype html>
<html lang="en">
<body>
  <form action="/submit" method="post">
    {{ csrf.field() }}
    <input type="text" name="message" placeholder="Your message">
    <button type="submit">Submit</button>
  </form>
</body>
</html>

Explanation: {{ csrf.field() }} inserts a hidden <input> containing the CSRF token. When the form is submitted, the browser sends that token along with the other form fields. csrf-kit validates it before Flask runs the /submit view. You should include this field in every form that changes data, including login, logout, account settings, and delete forms.

Without the field, or with an expired/invalid token, the request is rejected with status 400.

Protect JavaScript / fetch requests

For fetch() or AJAX requests, send the token as a request header instead of a form field:

<button id="save">Save</button>
<script>
const csrfToken = {{ csrf.token() | tojson }};

document.querySelector('#save').addEventListener('click', async () => {
  const response = await fetch('/submit', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Kit': csrfToken
    },
    body: JSON.stringify({ message: 'Hello' })
  });
  console.log(response.status, await response.text());
});
</script>

Explanation: csrf.token() returns a token for the current session. Jinja's tojson filter safely embeds it in JavaScript. The default header is X-CSRF-Kit. credentials: 'same-origin' allows the session cookie to accompany same-origin requests. The CSRF token is not a replacement for login or authorization checks. Render this snippet through Jinja (for example inside a .html template), not in a standalone static .js file.

Flask application factory

If your project uses create_app(), initialize the extension separately:

from flask import Flask
from csrf_kit import CSRF

csrf = CSRF()

def create_app():
    app = Flask(__name__)
    app.config.from_prefixed_env()
    csrf.install(app)
    return app

Explanation: CSRF() creates an unbound extension, and csrf.install(app) attaches it to an individual Flask application. With from_prefixed_env(), configure FLASK_SECRET_KEY in the environment. Install the extension once per app.

Configuration

You can customize the extension during installation:

csrf = CSRF()
csrf.install(
    app,
    ttl=1800,
    field_name="_csrf",
    header_name="X-CSRF-Kit",
    strict_origin=True,
    automatic=True,
)
Option Default Description
ttl 3600 Token lifetime in seconds (e.g., 1800 = 30 minutes).
field_name _csrf Name of the hidden HTML form field.
header_name X-CSRF-Kit Header used for JavaScript requests.
strict_origin True On HTTPS, require a matching Origin or Referer.
automatic True Automatically check unsafe requests.
signing_key None Optional separate secret used for CSRF signing; Flask SECRET_KEY is still required for sessions.

If you change field_name or header_name, update your forms or JavaScript accordingly. Avoid disabling strict_origin or automatic unless you understand the security consequences.

Available methods

  • csrf.field() — renders a complete hidden input in Jinja templates.
  • csrf.token() — generates/returns the current session's token; use it in Python or Jinja.
  • csrf.renew() — rotates the session's CSRF nonce and returns a fresh token. Call after a successful login or logout (once the session has been updated) to invalidate previously issued tokens.
  • csrf.verify(token) — checks one supplied token; it does not perform HTTP-method or origin checks.
  • csrf.guard() — checks the current unsafe request, including HTTPS origin checking when enabled. This runs automatically unless automatic=False.
  • @csrf.skip — exempts a specific Flask view from automatic checking.

If you turn off automatic checking, explicitly call csrf.guard() for each unsafe route you intend to protect.

Exempt a webhook endpoint

Some incoming webhooks cannot send your user's CSRF token. You can exempt only those routes:

from flask import request

@app.post('/webhook')
@csrf.skip
def webhook():
    # Verify the provider's webhook signature before processing the payload.
    return {'received': True}

Explanation: @csrf.skip skips automatic CSRF checking for this view. This does not authenticate the sender. Verify the webhook provider's signature or use another appropriate authentication method. Do not exempt ordinary user-facing forms just to fix a missing-token error. You can also apply csrf.skip(blueprint) to an entire blueprint, but that has a much wider effect.

Customize CSRF errors

By default, invalid requests raise Rejected, a subclass of Flask/Werkzeug's BadRequest (HTTP 400). Register an error handler for a user-friendly response:

from flask import jsonify
from csrf_kit import Rejected

@app.errorhandler(Rejected)
def handle_csrf_error(error):
    return jsonify(error="csrf_failed", message="Please refresh and try again."), 400

Explanation: Users may encounter a failure if a form has been left open longer than ttl, the session has changed, or the token is missing. Refreshing the page generates a current token. Avoid exposing internal verification details in production error messages.

Run the included example

From the project root, after installation:

flask --app examples.app run

Open http://127.0.0.1:5000 to test a protected HTML form and a JSON fetch() request. The bundled example uses a temporary secret if none is set for local demonstration only.

Security notes

  • Use a strong, stable SECRET_KEY and protect it as a secret; rotate it deliberately because rotation invalidates existing sessions/tokens.
  • Use HTTPS in production and enable appropriate Flask session cookie settings (SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, and a suitable SESSION_COOKIE_SAMESITE).
  • Configure and trust your reverse proxy correctly so Flask knows whether the original request used HTTPS and which host the user accessed.
  • Do not put CSRF tokens in URLs, logs, or publicly cached pages. Avoid shared caches for session-specific HTML containing tokens.
  • CSRF protection does not replace authentication, authorization, input validation, or protection against XSS.
  • Only exempt endpoints that have an independent, verified security mechanism.

Status: This is an alpha release and has not been independently security audited. csrf-kit is not affiliated with Flask-WTF or the Pallets project.

Contributors

Metadata

Release files for csrf-kit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for csrf-kit 0.1.0
File Size Uploaded
csrf_kit-0.1.0.tar.gz 13.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for csrf-kit 0.1.0
File Interpreter ABI Platform
csrf_kit-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 23.1 kB

Release files / csrf_kit-0.1.0.tar.gz

Download URL csrf_kit-0.1.0.tar.gz
Size 13.3 kB
Tags Source
SHA-256 checksum
How to use checksums
7288ba18ba1a3868b00fdfc1048f31ae688552d54d5ee67be13f235fee72424d
BLAKE2b-256 checksum
How to use checksums
524c9e73271d077c1a9c0f1cc00267569c7afc8ccedb71076769c92c31f2f518
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / csrf_kit-0.1.0-py3-none-any.whl

Download URL csrf_kit-0.1.0-py3-none-any.whl
Size 9.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5f7ee7852ebcf18defc0613aefdf595f32227bea7eadfcc68dc3614bd0748e6c
BLAKE2b-256 checksum
How to use checksums
c299898d4b9806ee5db2b62a7d650f2b758a4ddaa1d3a34613e4fdd3f1c90153
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page