diff --git a/CHANGES.rst b/CHANGES.rst index ecbca02..437eefe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,16 @@ Changes ======= +Version 2.0.0 (released 2024-12-03) + +- fix: set_cookie needs a str +- fix: cookie_jar not in FlaskClient +- tests: update api usage of set_cookie +- fix: set_cookie needs a str +- chore: remove unused imports +- global: remove try except for jws +- setup: bump major dependencies + Version v1.5.0 (released 2024-12-02) - global: make sentry-sdk optional diff --git a/invenio_rest/__init__.py b/invenio_rest/__init__.py index 506a192..ec966f1 100644 --- a/invenio_rest/__init__.py +++ b/invenio_rest/__init__.py @@ -196,6 +196,6 @@ from .ext import InvenioREST from .views import ContentNegotiatedMethodView -__version__ = "1.5.0" +__version__ = "2.0.0" __all__ = ("__version__", "csrf", "InvenioREST", "ContentNegotiatedMethodView") diff --git a/invenio_rest/csrf.py b/invenio_rest/csrf.py index 452d3a6..ba3cdba 100644 --- a/invenio_rest/csrf.py +++ b/invenio_rest/csrf.py @@ -15,22 +15,16 @@ """ -import re + import secrets import string from datetime import datetime, timedelta, timezone from urllib.parse import urlparse from flask import Blueprint, abort, current_app, request +from invenio_base.jws import TimedJSONWebSignatureSerializer from itsdangerous import BadSignature, SignatureExpired -try: - # itsdangerous < 2.1.0 - from itsdangerous import TimedJSONWebSignatureSerializer -except ImportError: - # itsdangerous >= 2.1.0 - from invenio_base.jws import TimedJSONWebSignatureSerializer - REASON_NO_REFERER = "Referer checking failed - no Referer." REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins." REASON_NO_CSRF_COOKIE = "CSRF cookie not set." @@ -71,7 +65,7 @@ def _get_new_csrf_token(expires_in=None): current_app.config["CSRF_ALLOWED_CHARS"], ) ) - return encoded_token + return encoded_token.decode("utf-8") def _get_csrf_token(): @@ -109,7 +103,7 @@ def _decode_csrf(data): def _set_token(response): response.set_cookie( current_app.config["CSRF_COOKIE_NAME"], - _get_new_csrf_token().decode("utf-8"), + _get_new_csrf_token(), max_age=current_app.config.get( # 1 week for cookie (but we rotate the token every day) "CSRF_COOKIE_MAX_AGE", diff --git a/setup.cfg b/setup.cfg index 1b01cad..045e963 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,16 +28,16 @@ python_requires = >=3.7 zip_safe = False install_requires = Flask-CORS>=2.1.0 - invenio-base>=1.2.5,<2.0.0 - invenio-logging>=2.1.0,<3.0.0 - itsdangerous>=1.1.0 + invenio-base>=2.0.0,<3.0.0 + invenio-logging>=4.0.0,<5.0.0 + itsdangerous>=2.2.0 marshmallow>=2.15.2 webargs>=5.5.0,<6.0.0 [options.extras_require] tests = pytest-black-ng>=0.4.0 - pytest-invenio>=1.4.0,<3.0.0 + pytest-invenio>=3.0.0,<4.0.0 xmltodict>=0.11.0 Sphinx>=4.5.0 # Kept for backwards compatibility diff --git a/tests/test_csrf.py b/tests/test_csrf.py index bcd3e31..585a68a 100644 --- a/tests/test_csrf.py +++ b/tests/test_csrf.py @@ -2,6 +2,7 @@ # # This file is part of Invenio. # Copyright (C) 2015-2024 CERN. +# Copyright (C) 2024 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -24,7 +25,6 @@ CSRFProtectMiddleware, _get_new_csrf_token, ) -from invenio_rest.ext import InvenioREST def test_csrf_init(): @@ -82,10 +82,8 @@ def test_csrf_enabled(csrf_app, csrf): CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] - cookie = next( - (cookie for cookie in client.cookie_jar if cookie.name == CSRF_COOKIE_NAME), - None, - ) + cookie = client.get_cookie(CSRF_COOKIE_NAME) + res = client.post( "/csrf-protected", data=json.dumps(dict(foo="bar")), @@ -95,10 +93,8 @@ def test_csrf_enabled(csrf_app, csrf): assert res.status_code == 200 # The CSRF token should not have changed. - new_cookie = next( - (cookie for cookie in client.cookie_jar if cookie.name == CSRF_COOKIE_NAME), - None, - ) + new_cookie = client.get_cookie(CSRF_COOKIE_NAME) + assert cookie.value == new_cookie.value @@ -210,7 +206,7 @@ def csrf_skip(): def test_csrf_not_signed_correctly(csrf_app, csrf): """Test CSRF malicious attempt with passing malicious cookie and header.""" - from itsdangerous import TimedJSONWebSignatureSerializer + from invenio_base.jws import TimedJSONWebSignatureSerializer with csrf_app.test_client() as client: # try to pass our own signed cookie and header in an attempt to bypass @@ -222,7 +218,9 @@ def test_csrf_not_signed_correctly(csrf_app, csrf): malicious_cookie = csrf_serializer.dumps({"user": "malicious"}, "my_secret") CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] - client.set_cookie("localhost", CSRF_COOKIE_NAME, malicious_cookie) + client.set_cookie( + CSRF_COOKIE_NAME, malicious_cookie.decode("utf-8"), domain="localhost" + ) res = client.post( "/csrf-protected", @@ -237,16 +235,14 @@ def test_csrf_not_signed_correctly(csrf_app, csrf): def test_csrf_token_rotation(csrf_app, csrf): """Test CSRF token rotation.""" - from itsdangerous import TimedJSONWebSignatureSerializer - with csrf_app.test_client() as client: CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] # Token in grace period - succeeds but token gets rotated expired_token = _get_new_csrf_token(expires_in=-1) - client.set_cookie("localhost", CSRF_COOKIE_NAME, expired_token) - old_cookie = {cookie.name: cookie for cookie in client.cookie_jar}["csrftoken"] + client.set_cookie(CSRF_COOKIE_NAME, expired_token, domain="localhost") + old_cookie = client.get_cookie(CSRF_COOKIE_NAME) res = client.post( "/csrf-protected", data=json.dumps(dict(foo="bar")), @@ -255,7 +251,7 @@ def test_csrf_token_rotation(csrf_app, csrf): ) assert res.status_code == 200 # Token was rotated and new requests succeeds - new_cookie = {cookie.name: cookie for cookie in client.cookie_jar}["csrftoken"] + new_cookie = client.get_cookie(CSRF_COOKIE_NAME) assert new_cookie.value != old_cookie.value res = client.post( "/csrf-protected", @@ -271,7 +267,7 @@ def test_csrf_token_rotation(csrf_app, csrf): content_type="application/json", headers={CSRF_HEADER_NAME: new_cookie.value}, ) - last_cookie = {cookie.name: cookie for cookie in client.cookie_jar}["csrftoken"] + last_cookie = client.get_cookie(CSRF_COOKIE_NAME) assert new_cookie.value == last_cookie.value assert res.status_code == 200 @@ -279,7 +275,7 @@ def test_csrf_token_rotation(csrf_app, csrf): # - Hack to have a negative grace period to force the error. csrf_app.config["CSRF_TOKEN_GRACE_PERIOD"] = -10000 expired_token = _get_new_csrf_token(expires_in=-60 * 60 * 24 * 14) - client.set_cookie("localhost", CSRF_COOKIE_NAME, expired_token) + client.set_cookie(CSRF_COOKIE_NAME, expired_token, domain="localhost") res = client.post( "/csrf-protected", data=json.dumps(dict(foo="bar")), @@ -302,10 +298,8 @@ def test_csrf_no_referer(csrf_app, csrf): CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] - cookie = next( - (cookie for cookie in client.cookie_jar if cookie.name == CSRF_COOKIE_NAME), - None, - ) + cookie = client.get_cookie(CSRF_COOKIE_NAME) + res = client.post( "/csrf-protected", base_url="https://localhost", @@ -330,10 +324,8 @@ def test_csrf_malformed_referer(csrf_app, csrf): CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] - cookie = next( - (cookie for cookie in client.cookie_jar if cookie.name == CSRF_COOKIE_NAME), - None, - ) + cookie = client.get_cookie(CSRF_COOKIE_NAME) + res = client.post( "/csrf-protected", base_url="https://localhost", @@ -358,10 +350,8 @@ def test_csrf_insecure_referer(csrf_app, csrf): CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] - cookie = next( - (cookie for cookie in client.cookie_jar if cookie.name == CSRF_COOKIE_NAME), - None, - ) + cookie = client.get_cookie(CSRF_COOKIE_NAME) + res = client.post( "/csrf-protected", base_url="https://localhost", @@ -389,10 +379,8 @@ def test_csrf_bad_referer(csrf_app, csrf): CSRF_COOKIE_NAME = csrf_app.config["CSRF_COOKIE_NAME"] CSRF_HEADER_NAME = csrf_app.config["CSRF_HEADER"] - cookie = next( - (cookie for cookie in client.cookie_jar if cookie.name == CSRF_COOKIE_NAME), - None, - ) + cookie = client.get_cookie(CSRF_COOKIE_NAME) + csrf_app.config["APP_ALLOWED_HOSTS"] = ["allowed-referer"] not_allowed_referer = "https://not-allowed-referer" res = client.post(