Giter Club home page Giter Club logo

starlette-csrf's Introduction

Starlette CSRF Middleware

Starlette middleware implementing Double Submit Cookie technique to mitigate CSRF.

build codecov PyPI version Downloads

How it works?

  1. The user makes a first request with a method considered safe (by default GET, HEAD, OPTIONS, TRACE).
  2. It receives in response a cookie (named by default csrftoken) which contains a secret value.
  3. When the user wants to make an unsafe request, the server expects them to send the same secret value in a header (named by default x-csrftoken).
  4. The middleware will then compare the secret value provided in the cookie and the header.
    • If they match, the request is processed.
    • Otherwise, a 403 Forbidden error response is given.

This mechanism is necessary if you rely on cookie authentication in a browser. You can have more information about CSRF and Double Submit Cookie in the OWASP Cheat Sheet Series.

Installation

pip install starlette-csrf

Usage with Starlette

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette_csrf import CSRFMiddleware

routes = ...

middleware = [
    Middleware(CSRFMiddleware, secret="__CHANGE_ME__")
]

app = Starlette(routes=routes, middleware=middleware)

Usage with FastAPI

from fastapi import FastAPI
from starlette_csrf import CSRFMiddleware

app = FastAPI()

app.add_middleware(CSRFMiddleware, secret="__CHANGE_ME__")

Arguments

  • secret (str): Secret to sign the CSRF token value. Be sure to choose a strong passphrase and keep it SECRET.
  • required_urls (Optional[List[re.Pattern]] - None): List of URL regexes that the CSRF check should always be enforced, no matter the method or the cookies present.
  • exempt_urls (Optional[List[re.Pattern]] - None): List of URL regexes that the CSRF check should be skipped on. Useful if you have any APIs that you know do not need CSRF protection.
  • sensitive_cookies (Set[str] - None): Set of cookie names that should trigger the CSRF check if they are present in the request. Useful if you have other authentication methods that don't rely on cookies and don't need CSRF enforcement. If this parameter is None, the default, CSRF is always enforced.
  • safe_methods (Set[str] - {"GET", "HEAD", "OPTIONS", "TRACE"}): HTTP methods considered safe which don't need CSRF protection.
  • cookie_name (str - csrftoken): Name of the cookie.
  • cookie_path str - /): Cookie path.
  • cookie_domain (Optional[str] - None): Cookie domain. If your frontend and API lives in different sub-domains, be sure to set this argument with your root domain to allow your frontend sub-domain to read the cookie on the JavaScript side.
  • cookie_secure (bool - False): Whether to only send the cookie to the server via SSL request.
  • cookie_samesite (str - lax): Samesite strategy of the cookie.
  • header_name (str - x-csrftoken): Name of the header where you should set the CSRF token.

Customize error response

By default, a plain text response with the status code 403 is returned when the CSRF verification is failing. You can customize it by overloading the middleware class and implementing the _get_error_response method. It accepts in argument the original Request object and expects a Response. For example:

from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette_csrf import CSRFMiddleware

class CustomResponseCSRFMiddleware(CSRFMiddleware):
    def _get_error_response(self, request: Request) -> Response:
        return JSONResponse(
            content={"code": "CSRF_ERROR"}, status_code=403
        )

Development

Setup environment

We use Hatch to manage the development environment and production build. Ensure it's installed on your system.

Run unit tests

You can run all the tests with:

hatch run test

Format the code

Execute the following command to apply linting and check typing:

hatch run lint

License

This project is licensed under the terms of the MIT license.

starlette-csrf's People

Contributors

frankie567 avatar lsapan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

starlette-csrf's Issues

WebSocket fails via CSRFMiddleware

Ideally, I'd like to use the CSRFMiddleware on a websocket route. But at present, the CSRFMiddleware makes hits an assertion error whenever the websocket route is accessed:

Error

.venv/lib/python3.11/site-packages/starlette/testclient.py:91: in __enter__
    message = self.receive()
.venv/lib/python3.11/site-packages/starlette/testclient.py:160: in receive
    raise message
.venv/lib/python3.11/site-packages/anyio/from_thread.py:219: in _call_func
    retval = await retval
.venv/lib/python3.11/site-packages/starlette/testclient.py:118: in _run
    await self.app(scope, receive, send)
.venv/lib/python3.11/site-packages/fastapi/applications.py:289: in __call__
    await super().__call__(scope, receive, send)
.venv/lib/python3.11/site-packages/starlette/applications.py:122: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.11/site-packages/starlette/middleware/errors.py:149: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.11/site-packages/starlette/middleware/cors.py:75: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.11/site-packages/starlette/middleware/sessions.py:86: in __call__
    await self.app(scope, receive, send_wrapper)
.venv/lib/python3.11/site-packages/starlette_csrf/middleware.py:55: in __call__
    request = Request(scope)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <starlette.requests.Request object at 0x137109750>
scope = {'app': <fastapi.applications.FastAPI object at 0x117817e10>, 'client': ['testclient', 50000], 'headers': [(b'host', '...'), (b'connection', b'upgrade'), ...], 'path': '/ws, ...}
receive = <function empty_receive at 0x102887600>, send = <function empty_send at 0x105c49b20>

    def __init__(
        self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send
    ):
        super().__init__(scope)
>       assert scope["type"] == "http"
E       AssertionError

.venv/lib/python3.11/site-packages/starlette/requests.py:197: AssertionError

Attempted solutions

  • Failed: Added my websocket route to exempt_urls -- think the error happens before this is every applied
  • Failed: Added my websocket route to required_urls -- I hoped that maybe the initial HTTP connection that gets upgrade would pass thru
  • Tried replacing request = Request(scope) with request = WebSocket(scope, receive=receive, send=send) if scope["type"] == "websocket" else Request(scope), but still occurred

Current workaround

I wrap CSRFMiddleware and only pass HTTP requests into it. This is suboptimal because I would like to enforce CSRF protection for my websocket route.

from starlette_csrf.middleware import CSRFMiddleware as _CSRFMiddleware
 
class CSRFMiddleware(_CSRFMiddleware):
    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        # type="websocket" will raise an exception at present
        if scope["type"] == "http":
            await super().__call__(scope, receive, send)
        else:
            await self.app(scope, receive, send)

Partial test case

I tried to document the error in a testcase, but using the httpx client does not expose the .websocket_connect() that starlette's testclient exposes. So this code does not yet fully work

def get_app(**middleware_kwargs) -> Starlette:
    async def get(request: Request):
        return JSONResponse({"hello": "world"})

    async def post(request: Request):
        json = await request.json()
        return JSONResponse(json)
    
    async def websocket(websocket: WebSocket):
        await websocket.accept()
        data = await websocket.receive_text()
        await websocket.send_text(data)
        await websocket.close()

    app = Starlette(
        debug=True,
        routes=[
            Route("/get", get, methods=["GET"]),
            Route("/post1", post, methods=["POST"]),
            Route("/post2", post, methods=["POST"]),
            WebSocketRoute("/ws", websocket),
        ],
        middleware=[Middleware(CSRFMiddleware, secret="SECRET", **middleware_kwargs)],
    )

    return app

@pytest.mark.asyncio
async def test_valid_websocket():
    async with get_test_client(get_app()) as client:
        response_get = await client.get("/get")
        csrf_cookie = response_get.cookies["csrftoken"]

        async with client.websocket_connect("/ws") as websocket:
            await websocket.send_text("hello world")
            data = await websocket.receive_text()
            assert data == "hello world"

Happy to try to help with some pointers.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

Hardcoded version of itsdangerous leads to dependency conflicts

The version of itsdangerous is hardcoded to 2.0.1 right now, which leads to issues in projects consuming starlette-csrf. Projects using Poetry can't install starlette-csrf if they already depend on a newer version of itsdangerous. If you install starlette-csrf via pip, you might end up with an outdated version of itsdangerous.

In my case I'm using Poetry and I already have the most recent version of itsdangerous (2.1.1) in my project. When I try to add starlette-csrf to my project, Poetry fails resolving the dependencies, because my project depends on a newer version of itsdangerous, but starlette-csrf requires a specific version of itsdangerous. In my specific case, I think I would have to downgrade itsdangerous to make everything work, but since itsdangerous is doing cryptographic stuff, I definetly want to use the latest version of it to get all (security) bug fixes.

I know not everyone is using Poetry but I can imagine other dependency managers might have a similar problem. Unfortunatly I don't know much about them and did not test this.

I tested to install starlette-crsf via pip directly and in this case you get itsdangerous version 2.0.1 if you don't specify a different version. Version 2.0.1 is outdated, but in my opinion you always want the latest version of itsdangerous to get all the latest (security) bug fixes.

I guess you specified this specific version of itsdangerous to ensure everything is working as expected, but i think it would be better to use some kind of version range like you already did with starlette. Maybe something like >=2.0.1,<3.0.0 would be a good idea? This way, users of starlette-csrf would get the most recent version of itsdangerous, but since the version is less than the next major release, breaking changes should not occure.

Jinja integration

Hello, thanks for your work on this lib! I'm using it in my FastAPI app and would like to know how I can integration the tokens with my Jinja templates like we do with Django. Is there a way?

Thanks

sensitive_cookies and preventing login CSRF

Hi again! Sorry for flooding you with issues lately, but I happen to work on a project using your fine suite of tools, so I stumble upon some things from time to time.

This time it's the following: starlette-csrf seems to work as expected in the context of my project, but there is one thing I think would be very useful but isn't possible ATM. The part of the OWASP Cheat Sheet you linked to that talks about double submit cookies has a passage that says

When a user visits (even before authenticating to prevent login CSRF), the site should generate a (cryptographically strong) pseudorandom value and set it as a cookie on the user's machine separate from the session identifier.

(emphasis by me) I may be misunderstanding login CSRF. I mean, there is no auth cookie before login. But the article makes it sound like it's a thing. So if my question misses the point, please help me out to understand it right :)

Now what I found is that starlette-csrf has a sensitive_cookies argument that defines the cookies that should trigger the CSRF check. This is very useful for me, as I want to allow cookie-based auth for my web client as well as JWT-based auth for external use of my server API. So I set sensitive_cookies to include the name of my authentication cookie (I use FastAPI-Users, as you know) to only enforce CSRF-checks when my auth cookie comes with a request via a method considered "unsafe".

The problem is: By doing this, I cannot prevent login CSRF anymore, because my auth cookie is only present in the client (web browser) after login. So calls to my login route won't have CSRF protection. There is exempt_urls to ignore calls to certain paths, but is there a way to enforce CSRF protection on requests to certain paths (like asgi-csrf's always_protect) even if the "sensitive cookie" is not present in the request? If there isn't (which I assume, as it's not documented), wouldn't that be a good addition to starlette-csrf's functionality?

As always: Thank you for your work!

CSRF validation fails after backend restart

Hey, I've been using your tooling for my projects

After a successful CSRF token set in cookies, if I refresh my server, the CSRF gets invalid. This would cause issues in production environments. Are there any solutions to this problem or am I missing something?

fails to install

Adding packages to default dependencies: starlette-csrf
โœ– ๐Ÿ”’ Lock failed
Unable to find a resolution for starlette because of the following conflicts:
  itsdangerous==2.0.0 (from <Candidate starlette-csrf 1.2.0 from https://pypi.org/simple/starlette-csrf/>)
  itsdangerous==2.0.0 (from <Candidate starlette-csrf 1.2.1 from https://pypi.org/simple/starlette-csrf/>)
  itsdangerous==2.0.1 (from <Candidate starlette-csrf 1.3.0 from https://pypi.org/simple/starlette-csrf/>)
  itsdangerous==2.0.1 (from <Candidate starlette-csrf 1.4.0 from https://pypi.org/simple/starlette-csrf/>)
  itsdangerous==2.0.1 (from <Candidate starlette-csrf 1.4.1 from https://pypi.org/simple/starlette-csrf/>)
  itsdangerous==2.0.1 (from <Candidate starlette-csrf 1.4.2 from https://pypi.org/simple/starlette-csrf/>)
  itsdangerous>=2.1.2 (from project)
  starlette<0.15.0,>=0.14.2 (from <Candidate starlette-csrf 1.0.0 from https://pypi.org/simple/starlette-csrf/>)
  starlette<0.20.0,>=0.14.2 (from <Candidate starlette-csrf 1.4.3 from https://pypi.org/simple/starlette-csrf/>)
  starlette>=0.20.3 (from project)
To fix this, you could loosen the dependency version constraints in pyproject.toml. See https://pdm.fming.dev/latest/usage/dependency//#solve-the-locking-failure for more details.
See /tmp/pdm-lock-ci4zv9xv.log for detailed debug log.
[ResolutionImpossible]: Unable to find a resolution
Add '-v' to see the detailed traceback

Response headers are only one-way? We never see x-csrftoken again.

A bit of confusion as to how this middleware is intended to work.

Response headers are one-way, correct? So how is x-csrftoken ever supposed to be seen by the server again without the client explicitly sending it?

Sending an invisible field with POST data makes sense, a custom header does not? Am I missing something?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.