modern-csrf 2026.10.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 George Macon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: modern-csrf
3
+ Version: 2026.10.20
4
+ Summary: Modern anti-CSRF middleware
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: asgi
10
+ Requires-Dist: asgiref>=3.10.0; extra == "asgi"
11
+ Dynamic: license-file
12
+
13
+ # Block Cross-Origin Requests
14
+
15
+ This project implements the modern [Cross-Site Request Forgery (CSRF)][csrf] defenses
16
+ as [described][valsorda] by Filippo Valsorda.
17
+ This is the [Fetch metadata approach][mdn-csrf-fetch] described on MDN.
18
+ Unsafe cross-origin requests are rejected with a 403 Forbidden status by default.
19
+
20
+ [csrf]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF
21
+ [valsorda]: https://words.filippo.io/csrf/
22
+ [mdn-csrf-fetch]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF#fetch_metadata
23
+
24
+ This approach protects users of browsers less than a few years old
25
+ (and anyone using an older browser than that likely has worse problems to worry about).
26
+ This approach is useful
27
+ because it can be cleanly implemented as a middleware
28
+ without requiring close coordination with the application.
29
+ That doesn't mean that you can't use CSRF tokens too.
30
+
31
+ ## How it works
32
+
33
+ The specific checks performed are:
34
+
35
+ 1. Allow requests with one of the safe HTTP methods: `GET`, `HEAD`, or `OPTIONS`.
36
+ 1. Allow requests from trusted origins.
37
+ 1. If the `Sec-Fetch-Site` header is set,
38
+ allow requests with `Sec-Fetch-Site` set to `same-origin` or `none`, and
39
+ reject other requests.
40
+ 1. Allow requests with neither of the `Sec-Fetch-Site` or `Origin` headers set.
41
+ 1. If the `Origin` and `Host` headers are both set,
42
+ allow requests where the host-and-port portion of the origin matches the host.
43
+ 1. Reject any remaining requests.
44
+
45
+ ## Things to watch out for
46
+
47
+ In order for your application to be protected by this middleware,
48
+ you *must not* change the application state
49
+ in response to a request with one of the ["safe" methods][safe]
50
+ (`GET`, `HEAD`, `OPTIONS`).
51
+ Note that this is not an additional requirement because
52
+ there are other places within the HTTP stack
53
+ where this assumption already exists.
54
+ For example,
55
+ requests with these methods may be automatically retried
56
+ without user intervention.
57
+
58
+ [safe]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
59
+
60
+ For this to work well,
61
+ your application should be served from a [trustworthy origin][trustworthy],
62
+ i.e. over HTTPS or via localhost.
63
+ There may be false rejections if you do not use a trustworthy origin.
64
+
65
+ [trustworthy]: https://www.w3.org/TR/secure-contexts/#is-origin-trustworthy
66
+
67
+ ## Protecting your application
68
+
69
+ Two versions of the middleware are provided:
70
+ a [Web Server Gateway Interface (WSGI)][wsgi] middleware
71
+ `modern_csrf.wsgi.BlockCrossOriginRequests`,
72
+ and an [Asynchronous Server Gateway Interface (ASGI)][asgi] middleware,
73
+ `modern_csrf.asgi.BlockCrossOriginRequests`.
74
+ Both versions have the same basic shape:
75
+ the middleware is constructed with the next application as the first argument,
76
+ and can be configured by setting keyword arguments.
77
+ The available configuration parameters are:
78
+
79
+ * `trusted_origins`:
80
+ A collection of origins from which requests are always allowed.
81
+ An "origin" is specified as `scheme://host[:port]` string
82
+ and must match *exactly*, including case.
83
+
84
+ Default: `()` (i.e. no trusted origins)
85
+
86
+ Example: `["https://example.com", "https://example.net:1234"]`
87
+
88
+ * `reject_handler`:
89
+ An application used to reject requests that should be rejected.
90
+
91
+ The default value returns a 403 Forbidden status with a fixed message.
92
+
93
+ * `log_rejected_requests`:
94
+ Should detailed information about rejected requests be logged?
95
+ For WSGI, the message is written to the WSGI error stream.
96
+ For ASGI, the message is logged via the standard library logging infrastructure.
97
+
98
+ Default: `False`
99
+
100
+ Example: `True`
101
+
102
+ [wsgi]: https://peps.python.org/pep-3333/
103
+ [asgi]: https://asgi.readthedocs.io/en/latest/
@@ -0,0 +1,91 @@
1
+ # Block Cross-Origin Requests
2
+
3
+ This project implements the modern [Cross-Site Request Forgery (CSRF)][csrf] defenses
4
+ as [described][valsorda] by Filippo Valsorda.
5
+ This is the [Fetch metadata approach][mdn-csrf-fetch] described on MDN.
6
+ Unsafe cross-origin requests are rejected with a 403 Forbidden status by default.
7
+
8
+ [csrf]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF
9
+ [valsorda]: https://words.filippo.io/csrf/
10
+ [mdn-csrf-fetch]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF#fetch_metadata
11
+
12
+ This approach protects users of browsers less than a few years old
13
+ (and anyone using an older browser than that likely has worse problems to worry about).
14
+ This approach is useful
15
+ because it can be cleanly implemented as a middleware
16
+ without requiring close coordination with the application.
17
+ That doesn't mean that you can't use CSRF tokens too.
18
+
19
+ ## How it works
20
+
21
+ The specific checks performed are:
22
+
23
+ 1. Allow requests with one of the safe HTTP methods: `GET`, `HEAD`, or `OPTIONS`.
24
+ 1. Allow requests from trusted origins.
25
+ 1. If the `Sec-Fetch-Site` header is set,
26
+ allow requests with `Sec-Fetch-Site` set to `same-origin` or `none`, and
27
+ reject other requests.
28
+ 1. Allow requests with neither of the `Sec-Fetch-Site` or `Origin` headers set.
29
+ 1. If the `Origin` and `Host` headers are both set,
30
+ allow requests where the host-and-port portion of the origin matches the host.
31
+ 1. Reject any remaining requests.
32
+
33
+ ## Things to watch out for
34
+
35
+ In order for your application to be protected by this middleware,
36
+ you *must not* change the application state
37
+ in response to a request with one of the ["safe" methods][safe]
38
+ (`GET`, `HEAD`, `OPTIONS`).
39
+ Note that this is not an additional requirement because
40
+ there are other places within the HTTP stack
41
+ where this assumption already exists.
42
+ For example,
43
+ requests with these methods may be automatically retried
44
+ without user intervention.
45
+
46
+ [safe]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
47
+
48
+ For this to work well,
49
+ your application should be served from a [trustworthy origin][trustworthy],
50
+ i.e. over HTTPS or via localhost.
51
+ There may be false rejections if you do not use a trustworthy origin.
52
+
53
+ [trustworthy]: https://www.w3.org/TR/secure-contexts/#is-origin-trustworthy
54
+
55
+ ## Protecting your application
56
+
57
+ Two versions of the middleware are provided:
58
+ a [Web Server Gateway Interface (WSGI)][wsgi] middleware
59
+ `modern_csrf.wsgi.BlockCrossOriginRequests`,
60
+ and an [Asynchronous Server Gateway Interface (ASGI)][asgi] middleware,
61
+ `modern_csrf.asgi.BlockCrossOriginRequests`.
62
+ Both versions have the same basic shape:
63
+ the middleware is constructed with the next application as the first argument,
64
+ and can be configured by setting keyword arguments.
65
+ The available configuration parameters are:
66
+
67
+ * `trusted_origins`:
68
+ A collection of origins from which requests are always allowed.
69
+ An "origin" is specified as `scheme://host[:port]` string
70
+ and must match *exactly*, including case.
71
+
72
+ Default: `()` (i.e. no trusted origins)
73
+
74
+ Example: `["https://example.com", "https://example.net:1234"]`
75
+
76
+ * `reject_handler`:
77
+ An application used to reject requests that should be rejected.
78
+
79
+ The default value returns a 403 Forbidden status with a fixed message.
80
+
81
+ * `log_rejected_requests`:
82
+ Should detailed information about rejected requests be logged?
83
+ For WSGI, the message is written to the WSGI error stream.
84
+ For ASGI, the message is logged via the standard library logging infrastructure.
85
+
86
+ Default: `False`
87
+
88
+ Example: `True`
89
+
90
+ [wsgi]: https://peps.python.org/pep-3333/
91
+ [asgi]: https://asgi.readthedocs.io/en/latest/
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "modern-csrf"
3
+ version = "2026.10.20"
4
+ description = "Modern anti-CSRF middleware"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = []
10
+
11
+ [project.optional-dependencies]
12
+ asgi = [
13
+ "asgiref>=3.10.0",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["setuptools >= 77.0.3"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "mypy>=1.18.2",
23
+ "python-lsp-ruff>=2.3.0",
24
+ "python-lsp-server>=1.13.1",
25
+ ]
26
+
27
+ [tool.ruff.lint]
28
+ select = ["E4", "E7", "E9", "F", "I"]
29
+
30
+ [tool.mypy]
31
+ files = [
32
+ "src/**/*.py",
33
+ ]
34
+ disallow_untyped_defs = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,96 @@
1
+ import logging
2
+ from collections.abc import Awaitable, Container
3
+
4
+ from asgiref.compatibility import guarantee_single_callable
5
+ from asgiref.typing import (
6
+ ASGI3Application,
7
+ ASGIApplication,
8
+ ASGIReceiveCallable,
9
+ ASGISendCallable,
10
+ HTTPScope,
11
+ Scope,
12
+ )
13
+
14
+ from modern_csrf.base import should_accept_request
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ async def default_reject_handler(
20
+ scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
21
+ ) -> None:
22
+ await send(
23
+ {
24
+ "type": "http.response.start",
25
+ "status": 403,
26
+ "headers": [(b"Content-Type", b"text/plain")],
27
+ "trailers": False,
28
+ }
29
+ )
30
+ await send(
31
+ {
32
+ "type": "http.response.body",
33
+ "body": b"Unexpected cross-origin request forbidden.",
34
+ "more_body": False,
35
+ }
36
+ )
37
+
38
+
39
+ class BlockCrossOriginRequests:
40
+ app: ASGI3Application
41
+ trusted_origins: Container[str]
42
+ reject_handler: ASGI3Application
43
+ log_rejected_requests: bool
44
+ _asgi_single_callable: bool
45
+
46
+ def __init__(
47
+ self,
48
+ app: ASGIApplication,
49
+ *,
50
+ trusted_origins: Container[str] = (),
51
+ reject_handler: ASGIApplication = default_reject_handler,
52
+ log_rejected_requests: bool = False,
53
+ ) -> None:
54
+ self.app = guarantee_single_callable(app)
55
+ self.trusted_origins = trusted_origins
56
+ self.reject_handler = guarantee_single_callable(reject_handler)
57
+ self.log_rejected_requests = log_rejected_requests
58
+
59
+ # This is a hint to asgiref,
60
+ # which would otherwise be confused by our `__call__` not being async.
61
+ self._asgi_single_callable = True
62
+
63
+ def __call__(
64
+ self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
65
+ ) -> Awaitable[None]:
66
+ if scope["type"] != "http":
67
+ return self.app(scope, receive, send)
68
+
69
+ http_scope: HTTPScope = scope
70
+
71
+ method = http_scope["method"]
72
+ origin: str | None = None
73
+ sec_fetch_site: str | None = None
74
+ host: str | None = None
75
+
76
+ for name, value in http_scope["headers"]:
77
+ name = name.lower()
78
+ if name == b"origin":
79
+ origin = value.decode("ascii")
80
+ elif name == b"sec-fetch-site":
81
+ sec_fetch_site = value.decode("ascii")
82
+ elif name == b"host":
83
+ host = value.decode("ascii")
84
+
85
+ if should_accept_request(
86
+ method=method,
87
+ origin=origin,
88
+ sec_fetch_site=sec_fetch_site,
89
+ host=host,
90
+ trusted_origins=self.trusted_origins,
91
+ ):
92
+ return self.app(scope, receive, send)
93
+ else:
94
+ if self.log_rejected_requests:
95
+ log.error("Rejecting unexpected cross-origin request: %r", scope)
96
+ return self.reject_handler(scope, receive, send)
@@ -0,0 +1,49 @@
1
+ from collections.abc import Container
2
+
3
+
4
+ def should_accept_request(
5
+ method: str,
6
+ origin: str | None,
7
+ sec_fetch_site: str | None,
8
+ host: str | None,
9
+ trusted_origins: Container[str],
10
+ ) -> bool:
11
+ """Should the HTTP request be accepted?
12
+
13
+ This evaluates a request based on the fetch metadata headers
14
+ and returns ``True`` if it should be accepted and ``False`` otherwise.
15
+ Requests
16
+
17
+ 1. Should be accepted if they use a safe HTTP method.
18
+ 2. Should be accepted if they are from a trusted origin.
19
+ 3. Should be accepted
20
+ if the browser indicates that it is not a cross-origin request,
21
+ and rejected if the browser indicates that it is a cross-origin request.
22
+ 4. Should be accepted if the request is not from a web browser.
23
+ 5. Should be accepted if the host matches the origin.
24
+ (Since non-trustworthy origins (i.e. http:) don't send the Sec-* headers.)
25
+
26
+ all other requests are rejected.
27
+
28
+ """
29
+ if method in ("GET", "HEAD", "OPTIONS"):
30
+ return True
31
+
32
+ if origin in trusted_origins:
33
+ return True
34
+
35
+ if sec_fetch_site is not None:
36
+ if sec_fetch_site in ("same-origin", "none"):
37
+ return True
38
+ else:
39
+ return False
40
+
41
+ if sec_fetch_site is None and origin is None:
42
+ return True
43
+
44
+ if origin is not None and host is not None:
45
+ scheme, sep, host_with_port = origin.partition("://")
46
+ if host_with_port == host:
47
+ return True
48
+
49
+ return False
File without changes
@@ -0,0 +1,56 @@
1
+ from collections.abc import Container, Iterable
2
+ from wsgiref.types import ErrorStream, StartResponse, WSGIApplication, WSGIEnvironment
3
+
4
+ from modern_csrf.base import should_accept_request
5
+
6
+
7
+ def default_reject_handler(
8
+ environ: WSGIEnvironment, start_response: StartResponse
9
+ ) -> Iterable[bytes]:
10
+ start_response("403 Forbidden", [("Content-Type", "text/plain")])
11
+ return [b"Unexpected cross-origin request forbidden."]
12
+
13
+
14
+ class BlockCrossOriginRequests:
15
+ app: WSGIApplication
16
+ trusted_origins: Container[str]
17
+ reject_handler: WSGIApplication
18
+ log_rejected_requests: bool
19
+
20
+ def __init__(
21
+ self,
22
+ app: WSGIApplication,
23
+ *,
24
+ trusted_origins: Container[str] = (),
25
+ reject_handler: WSGIApplication = default_reject_handler,
26
+ log_rejected_requests: bool = False,
27
+ ) -> None:
28
+ self.app = app
29
+ self.trusted_origins = trusted_origins
30
+ self.reject_handler = reject_handler
31
+ self.log_rejected_requests = log_rejected_requests
32
+
33
+ def __call__(
34
+ self, environ: WSGIEnvironment, start_response: StartResponse
35
+ ) -> Iterable[bytes]:
36
+ method = environ["REQUEST_METHOD"]
37
+ origin = environ.get("HTTP_ORIGIN")
38
+ sec_fetch_site = environ.get("HTTP_SEC_FETCH_SITE")
39
+ host = environ.get("HTTP_HOST")
40
+
41
+ if should_accept_request(
42
+ method=method,
43
+ origin=origin,
44
+ sec_fetch_site=sec_fetch_site,
45
+ host=host,
46
+ trusted_origins=self.trusted_origins,
47
+ ):
48
+ return self.app(environ, start_response)
49
+ else:
50
+ if self.log_rejected_requests:
51
+ errors: ErrorStream = environ["wsgi.errors"]
52
+ errors.write(
53
+ f"Rejecting unexpected cross-origin request: {environ!r}\n"
54
+ )
55
+ errors.flush()
56
+ return self.reject_handler(environ, start_response)
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: modern-csrf
3
+ Version: 2026.10.20
4
+ Summary: Modern anti-CSRF middleware
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: asgi
10
+ Requires-Dist: asgiref>=3.10.0; extra == "asgi"
11
+ Dynamic: license-file
12
+
13
+ # Block Cross-Origin Requests
14
+
15
+ This project implements the modern [Cross-Site Request Forgery (CSRF)][csrf] defenses
16
+ as [described][valsorda] by Filippo Valsorda.
17
+ This is the [Fetch metadata approach][mdn-csrf-fetch] described on MDN.
18
+ Unsafe cross-origin requests are rejected with a 403 Forbidden status by default.
19
+
20
+ [csrf]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF
21
+ [valsorda]: https://words.filippo.io/csrf/
22
+ [mdn-csrf-fetch]: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/CSRF#fetch_metadata
23
+
24
+ This approach protects users of browsers less than a few years old
25
+ (and anyone using an older browser than that likely has worse problems to worry about).
26
+ This approach is useful
27
+ because it can be cleanly implemented as a middleware
28
+ without requiring close coordination with the application.
29
+ That doesn't mean that you can't use CSRF tokens too.
30
+
31
+ ## How it works
32
+
33
+ The specific checks performed are:
34
+
35
+ 1. Allow requests with one of the safe HTTP methods: `GET`, `HEAD`, or `OPTIONS`.
36
+ 1. Allow requests from trusted origins.
37
+ 1. If the `Sec-Fetch-Site` header is set,
38
+ allow requests with `Sec-Fetch-Site` set to `same-origin` or `none`, and
39
+ reject other requests.
40
+ 1. Allow requests with neither of the `Sec-Fetch-Site` or `Origin` headers set.
41
+ 1. If the `Origin` and `Host` headers are both set,
42
+ allow requests where the host-and-port portion of the origin matches the host.
43
+ 1. Reject any remaining requests.
44
+
45
+ ## Things to watch out for
46
+
47
+ In order for your application to be protected by this middleware,
48
+ you *must not* change the application state
49
+ in response to a request with one of the ["safe" methods][safe]
50
+ (`GET`, `HEAD`, `OPTIONS`).
51
+ Note that this is not an additional requirement because
52
+ there are other places within the HTTP stack
53
+ where this assumption already exists.
54
+ For example,
55
+ requests with these methods may be automatically retried
56
+ without user intervention.
57
+
58
+ [safe]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
59
+
60
+ For this to work well,
61
+ your application should be served from a [trustworthy origin][trustworthy],
62
+ i.e. over HTTPS or via localhost.
63
+ There may be false rejections if you do not use a trustworthy origin.
64
+
65
+ [trustworthy]: https://www.w3.org/TR/secure-contexts/#is-origin-trustworthy
66
+
67
+ ## Protecting your application
68
+
69
+ Two versions of the middleware are provided:
70
+ a [Web Server Gateway Interface (WSGI)][wsgi] middleware
71
+ `modern_csrf.wsgi.BlockCrossOriginRequests`,
72
+ and an [Asynchronous Server Gateway Interface (ASGI)][asgi] middleware,
73
+ `modern_csrf.asgi.BlockCrossOriginRequests`.
74
+ Both versions have the same basic shape:
75
+ the middleware is constructed with the next application as the first argument,
76
+ and can be configured by setting keyword arguments.
77
+ The available configuration parameters are:
78
+
79
+ * `trusted_origins`:
80
+ A collection of origins from which requests are always allowed.
81
+ An "origin" is specified as `scheme://host[:port]` string
82
+ and must match *exactly*, including case.
83
+
84
+ Default: `()` (i.e. no trusted origins)
85
+
86
+ Example: `["https://example.com", "https://example.net:1234"]`
87
+
88
+ * `reject_handler`:
89
+ An application used to reject requests that should be rejected.
90
+
91
+ The default value returns a 403 Forbidden status with a fixed message.
92
+
93
+ * `log_rejected_requests`:
94
+ Should detailed information about rejected requests be logged?
95
+ For WSGI, the message is written to the WSGI error stream.
96
+ For ASGI, the message is logged via the standard library logging infrastructure.
97
+
98
+ Default: `False`
99
+
100
+ Example: `True`
101
+
102
+ [wsgi]: https://peps.python.org/pep-3333/
103
+ [asgi]: https://asgi.readthedocs.io/en/latest/
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/modern_csrf/__init__.py
5
+ src/modern_csrf/asgi.py
6
+ src/modern_csrf/base.py
7
+ src/modern_csrf/py.typed
8
+ src/modern_csrf/wsgi.py
9
+ src/modern_csrf.egg-info/PKG-INFO
10
+ src/modern_csrf.egg-info/SOURCES.txt
11
+ src/modern_csrf.egg-info/dependency_links.txt
12
+ src/modern_csrf.egg-info/requires.txt
13
+ src/modern_csrf.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [asgi]
3
+ asgiref>=3.10.0