From 05b1f31645d03fa283722345e2342912c4ab4073 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Fri, 28 Aug 2026 09:34:58 +0530 Subject: [PATCH] Exempt nested blueprints when the parent is CSRF-exempt. Walking dotted blueprint names means csrf.exempt(api) also covers api.meta. --- docs/changes.rst | 8 ++++++++ docs/csrf.rst | 2 ++ src/flask_wtf/csrf.py | 17 +++++++++++++++-- tests/test_csrf_extension.py | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d76b3a61..c9d19af0 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,14 @@ Changes ======= +Version 1.3.1 +------------- + +Unreleased + +- Nested blueprints inherit CSRF exemption from an exempt parent. + :issue:`697` + Version 1.3.0 ------------- diff --git a/docs/csrf.rst b/docs/csrf.rst index 2efac16c..760e879a 100644 --- a/docs/csrf.rst +++ b/docs/csrf.rst @@ -142,6 +142,8 @@ You can exclude all the views of a blueprint. :: csrf.exempt(account_blueprint) +If that blueprint has nested children, they are exempt as well. + You can disable CSRF protection in all views by default, by setting ``WTF_CSRF_CHECK_DEFAULT`` to ``False``, and selectively call :meth:`~flask_wtf.csrf.CSRFProtect.protect` only when you need. This also enables you to do some diff --git a/src/flask_wtf/csrf.py b/src/flask_wtf/csrf.py index c3f1a4b0..ca5d8c94 100644 --- a/src/flask_wtf/csrf.py +++ b/src/flask_wtf/csrf.py @@ -300,8 +300,17 @@ def protect(self, apply_exemptions=False): g.csrf_valid = True # mark this request as CSRF valid def _is_exempt(self): - if current_app.blueprints.get(request.blueprint) in self._exempt_blueprints: - return True + bp_name = request.blueprint + if bp_name: + # Nested blueprints use dotted names (parent.child). Exempting a + # parent should also skip CSRF on its children. + name = bp_name + while True: + if current_app.blueprints.get(name) in self._exempt_blueprints: + return True + if "." not in name: + break + name = name.rsplit(".", 1)[0] view = current_app.view_functions.get(request.endpoint) if view is None: @@ -325,6 +334,10 @@ def some_view(): bp = Blueprint(...) csrf.exempt(bp) + Nested blueprints inherit exemption from a parent passed here. + + .. versionchanged:: 1.3.1 + Exempting a parent blueprint also exempts nested child blueprints. """ if isinstance(view, Blueprint): diff --git a/tests/test_csrf_extension.py b/tests/test_csrf_extension.py index 2248278d..68347e81 100644 --- a/tests/test_csrf_extension.py +++ b/tests/test_csrf_extension.py @@ -247,6 +247,22 @@ def index(): assert response.status_code == 200 +def test_exempt_parent_covers_nested_blueprint(app, csrf, client): + parent = Blueprint("api", __name__, url_prefix="/api") + child = Blueprint("meta", __name__, url_prefix="/meta") + csrf.exempt(parent) + + @child.route("/", methods=["POST"]) + def index(): + return "ok" + + parent.register_blueprint(child) + app.register_blueprint(parent) + + response = client.post("/api/meta/") + assert response.status_code == 200 + + def test_error_handler(app, client): @app.errorhandler(CSRFError) def handle_csrf_error(e):