Skip to content

Commit

Permalink
Only set csrftoken cookie if needed by page, closes #7
Browse files Browse the repository at this point in the history
scope['csrftoken'] is now a function that returns a string.

Calling that function lets the middleware know it needs to set the cookie.
  • Loading branch information
simonw committed Jun 5, 2020
1 parent 6f27156 commit 849b397
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 9 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,19 @@ from .my_asgi_app import app
app = asgi_csrf(app, signing_secret="secret-goes-here")
```

The middleware will set a `csrftoken` cookie, if one is missing. The value of that token will be made available as `scope["csrftoken]` to your ASGI application.
The middleware will set a `csrftoken` cookie, if one is missing. The value of that token will be made available to your ASGI application through the `scope["csrftoken"]` function.

Your application code should include that value as a hidden form field in any POST forms:

```html
<form action="/login" method="POST">
...
<input type="hidden" name="csrftoken" value="{{ request.scope.csrftoken }}">
<input type="hidden" name="csrftoken" value="{{ request.scope.csrftoken() }}">
</form>
```

Note that `request.scope["csrftoken"]` is a function that returns a string. Calling that function also lets the middleware know that the cookie should be set by that page, if the user does not already have that cookie.

The middleware will return a 403 forbidden error for any POST requests that do not include the matching `csrftoken` - either in the POST data or in a `x-csrftoken` HTTP header (useful for JavaScript `fetch()` calls).

The `signing_secret` is used to sign the tokens, to protect against subdomain vulnerabilities.
Expand Down
17 changes: 11 additions & 6 deletions asgi_csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,26 @@ def _asgi_csrf_decorator(app):
async def app_wrapped_with_csrf(scope, receive, send):
cookies = cookies_from_scope(scope)
csrftoken = None
has_csrftoken_cookie = False
should_set_cookie = False
should_set_cookie = True
if cookie_name in cookies:
try:
csrftoken = cookies.get(cookie_name, "")
signer.loads(csrftoken, signing_namespace)
except BadSignature:
csrftoken = ""
should_set_cookie = True
else:
should_set_cookie = False
if should_set_cookie:
# We are going to set that cookie
has_csrftoken_cookie = True
if not has_csrftoken_cookie:
csrftoken = signer.dumps(make_secret(16), signing_namespace)
scope = {**scope, **{SCOPE_KEY: csrftoken}}

def get_csrftoken():
nonlocal should_set_cookie
if not has_csrftoken_cookie:
should_set_cookie = True
return csrftoken

scope = {**scope, **{SCOPE_KEY: get_csrftoken}}

async def wrapped_send(event):
if event["type"] == "http.response.start":
Expand Down
21 changes: 20 additions & 1 deletion test_asgi_csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,24 @@


async def hello_world(request):
if "csrftoken" in request.scope:
print(request.scope["csrftoken"]())
if request.method == "POST":
data = await request.form()
return JSONResponse(dict(await request.form()))
return JSONResponse({"hello": "world"})


hello_world_app = Starlette(routes=[Route("/", hello_world, methods=["GET", "POST"]),])
async def hello_world_static(request):
return JSONResponse({"hello": "world", "static": True})


hello_world_app = Starlette(
routes=[
Route("/", hello_world, methods=["GET", "POST"]),
Route("/static", hello_world_static, methods=["GET"]),
]
)


@pytest.fixture
Expand Down Expand Up @@ -46,6 +57,14 @@ async def test_asgi_csrf_sets_cookie(app_csrf):
assert response.headers["set-cookie"].endswith("; Path=/")


@pytest.mark.asyncio
async def test_asgi_csrf_sets_no_cookie_if_page_has_no_form(app_csrf):
async with httpx.AsyncClient(app=app_csrf) as client:
response = await client.get("http://localhost/static")
assert b'{"hello":"world","static":true}' == response.content
assert "csrftoken" not in response.cookies


@pytest.mark.asyncio
async def test_asgi_csrf_does_not_set_cookie_if_one_sent(app_csrf, csrftoken):
async with httpx.AsyncClient(app=app_csrf) as client:
Expand Down

0 comments on commit 849b397

Please sign in to comment.