Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bump major #142

Merged
merged 8 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion invenio_rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
14 changes: 4 additions & 10 deletions invenio_rest/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,16 @@
<https://github.com/django/django/blob/master/django/middleware/csrf.py>
"""

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."
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 22 additions & 34 deletions tests/test_csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,7 +25,6 @@
CSRFProtectMiddleware,
_get_new_csrf_token,
)
from invenio_rest.ext import InvenioREST


def test_csrf_init():
Expand Down Expand Up @@ -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")),
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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")),
Expand All @@ -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",
Expand All @@ -271,15 +267,15 @@ 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

# Token outside grace period
# - 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")),
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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(
Expand Down