Giter Club home page Giter Club logo

asgi-csrf's Introduction

asgi-csrf's People

Contributors

simonw 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

Watchers

 avatar  avatar  avatar  avatar

asgi-csrf's Issues

Only set missing csrftoken cookie if needed by current page

See comment on simonw/datasette#798 (comment) - right now this middleware sets the csrftoken cookie if it is missing on EVERY page.

This is bad, because it doesn't take caching into account. Pages should not be cached by Varnish/CloudFlare etc if they are setting a secret cookie value!

Instead, we should do what Django does. Here's a snippet from the Django docs on CSRF and caching: https://docs.djangoproject.com/en/3.0/ref/csrf/#caching

If the csrf_token template tag is used by a template (or the get_token function is called some other way), CsrfViewMiddleware will add a cookie and a Vary: Cookie header to the response. This means that the middleware will play well with the cache middleware if it is used as instructed

Always generate a token even if scope not called

Hello 👋

Thank you for this very useful middleware!

I have a use case (see fastapi-users/fastapi-users#291) where I call an API through the browser, using Cookie authentication. Thus, CSRF protection would be beneficial. However, it's a pure API : it doesn't generate any template ; so I don't have the opportunity to call request.scope.csrftoken() to generate the token.

Would it be possible (and sensible!) to have an option in the middleware to allow a token to be generated even if request.scope.csrftoken() is not called in the route logic?

Best regards!

Skip CSRF checks if no cookies or if authorization: bearer xxx headers

Needed by Datasette in simonw/datasette#835

If an incoming request has no cookies there's no point in CSRF protecting it... UNLESS it's to a login form to protect againts login CSRF attacks. So the middleware should have an option for "always CSRF protect these paths" to allow /login to be protected.

If an incoming request has a Authorization: Bearer xxx token there's no need to CSRF protect it because regular user requests from authenticated browsers can't include the Bearer prefix - they will always look like this instead (which should be CSRF protected):

Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

Bug: I'm not correctly passing through headers

Consider this code:

asgi-csrf/asgi_csrf.py

Lines 63 to 76 in a0647ad

new_headers = []
if page_needs_vary_cookie:
# Loop through original headers, modify or add "vary"
found_vary = False
for key, value in original_headers:
if key == b"vary":
found_vary = True
vary_bits = [v.strip() for v in value.split(b",")]
if b"Cookie" not in vary_bits:
vary_bits.append(b"Cookie")
value = b", ".join(vary_bits)
new_headers.append((key, value))
if not found_vary:
new_headers.append((b"vary", b"Cookie"))

If page_needs_vary_cookie is False, the existing headers are not copied over.

2 test_multipart tests failures

Hello!

Running the test suite, I get:

starting phase `check'
Using pytest
============================= test session starts ==============================
platform linux -- Python 3.9.9, pytest-6.2.5, py-1.10.0, pluggy-0.13.1 -- /gnu/store/slsh0qjv5j68xda2bb6h8gsxwyi1j25a-python-wrapper-3.9.9/bin/python
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('/tmp/guix-build-python-asgi-csrf-0.9.drv-0/source/.hypothesis/examples')
rootdir: /tmp/guix-build-python-asgi-csrf-0.9.drv-0/source
plugins: hypothesis-6.0.2, asyncio-0.17.2, anyio-3.5.0
asyncio: mode=legacy
collecting ... collected 32 items

test_asgi_csrf.py::test_hello_world_app PASSED                           [  3%]
test_asgi_csrf.py::test_signing_secret_if_none_provided PASSED           [  6%]
test_asgi_csrf.py::test_asgi_csrf_sets_cookie PASSED                     [  9%]
test_asgi_csrf.py::test_asgi_csrf_modifies_existing_vary_header PASSED   [ 12%]
test_asgi_csrf.py::test_asgi_csrf_sets_no_cookie_or_vary_if_page_has_no_form PASSED [ 15%]
test_asgi_csrf.py::test_vary_header_only_if_page_contains_csrftoken PASSED [ 18%]
test_asgi_csrf.py::test_headers_passed_through_correctly PASSED          [ 21%]
test_asgi_csrf.py::test_asgi_csrf_does_not_set_cookie_if_one_sent PASSED [ 25%]
test_asgi_csrf.py::test_prevents_post_if_cookie_not_sent_in_post PASSED  [ 28%]
test_asgi_csrf.py::test_allows_post_if_cookie_duplicated_in_header PASSED [ 31%]
test_asgi_csrf.py::test_allows_post_if_cookie_duplicated_in_post_data PASSED [ 34%]
test_asgi_csrf.py::test_multipart FAILED                                 [ 37%]
test_asgi_csrf.py::test_multipart_failure_wrong_token FAILED             [ 40%]
test_asgi_csrf.py::test_multipart_failure_missing_token PASSED           [ 43%]
test_asgi_csrf.py::test_multipart_failure_file_comes_before_token PASSED [ 46%]
test_asgi_csrf.py::test_post_with_authorization[Bearer xxx-200] PASSED   [ 50%]
test_asgi_csrf.py::test_post_with_authorization[Basic xxx-403] PASSED    [ 53%]
test_asgi_csrf.py::test_no_cookies_skips_check_unless_path_required[cookies0-/-200] PASSED [ 56%]
test_asgi_csrf.py::test_no_cookies_skips_check_unless_path_required[cookies1-/-403] PASSED [ 59%]
test_asgi_csrf.py::test_no_cookies_skips_check_unless_path_required[cookies2-/login-403] PASSED [ 62%]
test_asgi_csrf.py::test_no_cookies_skips_check_unless_path_required[cookies3-/login-403] PASSED [ 65%]
test_asgi_csrf.py::test_skip_if_scope[cookies0-/-200] PASSED             [ 68%]
test_asgi_csrf.py::test_skip_if_scope[cookies1-/-403] PASSED             [ 71%]
test_asgi_csrf.py::test_skip_if_scope[cookies2-/api/-200] PASSED         [ 75%]
test_asgi_csrf.py::test_skip_if_scope[cookies3-/api/-200] PASSED         [ 78%]
test_asgi_csrf.py::test_skip_if_scope[cookies4-/api/foo-200] PASSED      [ 81%]
test_asgi_csrf.py::test_skip_if_scope[cookies5-/api/foo-200] PASSED      [ 84%]
test_asgi_csrf.py::test_always_set_cookie[True] PASSED                   [ 87%]
test_asgi_csrf.py::test_always_set_cookie[False] PASSED                  [ 90%]
test_asgi_csrf.py::test_always_set_cookie_unless_cookie_is_set[True] PASSED [ 93%]
test_asgi_csrf.py::test_always_set_cookie_unless_cookie_is_set[False] PASSED [ 96%]
test_asgi_csrf.py::test_asgi_lifespan PASSED                             [100%]

=================================== FAILURES ===================================
________________________________ test_multipart ________________________________

csrftoken = 'InRva2VuIg.49BUIh1HVBjcyCpg_4018iFDFdY'

    @pytest.mark.asyncio
    async def test_multipart(csrftoken):
        async with httpx.AsyncClient(
            app=asgi_csrf(hello_world_app, signing_secret=SECRET)
        ) as client:
>           response = await client.post(
                "http://localhost/",
                data={"csrftoken": csrftoken},
                files={"csv": ("data.csv", "blah,foo\n1,2", "text/csv")},
                cookies={"csrftoken": csrftoken},
            )

test_asgi_csrf.py:186: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_client.py:1842: in post
    return await self.request(
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_client.py:1514: in request
    request = self.build_request(
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_client.py:356: in build_request
    return Request(
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_models.py:336: in __init__
    headers, stream = encode_request(content, data, files, json)
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_content.py:210: in encode_request
    return encode_multipart_data(data or {}, files, boundary)
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_content.py:155: in encode_multipart_data
    multipart = MultipartStream(data=data, files=files, boundary=boundary)
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_multipart.py:188: in __init__
    self.fields = list(self._iter_fields(data, files))
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_multipart.py:202: in _iter_fields
    yield FileField(name=name, value=value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <httpx._multipart.FileField object at 0x7ffff5856f40>, name = 'csv'
value = ('data.csv', 'blah,foo\n1,2', 'text/csv')

    def __init__(self, name: str, value: FileTypes) -> None:
        self.name = name
    
        fileobj: FileContent
    
        headers: typing.Dict[str, str] = {}
        content_type: typing.Optional[str] = None
    
        # This large tuple based API largely mirror's requests' API
        # It would be good to think of better APIs for this that we could include in httpx 2.0
        # since variable length tuples (especially of 4 elements) are quite unwieldly
        if isinstance(value, tuple):
            if len(value) == 2:
                # neither the 3rd parameter (content_type) nor the 4th (headers) was included
                filename, fileobj = value  # type: ignore
            elif len(value) == 3:
                filename, fileobj, content_type = value  # type: ignore
            else:
                # all 4 parameters included
                filename, fileobj, content_type, headers = value  # type: ignore
        else:
            filename = Path(str(getattr(value, "name", "upload"))).name
            fileobj = value
    
        if content_type is None:
            content_type = guess_content_type(filename)
    
        has_content_type_header = any("content-type" in key.lower() for key in headers)
        if content_type is not None and not has_content_type_header:
            # note that unlike requests, we ignore the content_type
            # provided in the 3rd tuple element if it is also included in the headers
            # requests does the opposite (it overwrites the header with the 3rd tuple element)
            headers["Content-Type"] = content_type
    
        if isinstance(fileobj, (str, io.StringIO)):
>           raise TypeError(f"Expected bytes or bytes-like object got: {type(fileobj)}")
E           TypeError: Expected bytes or bytes-like object got: <class 'str'>

/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_multipart.py:111: TypeError
______________________ test_multipart_failure_wrong_token ______________________

csrftoken = 'InRva2VuIg.49BUIh1HVBjcyCpg_4018iFDFdY'

    @pytest.mark.asyncio
    async def test_multipart_failure_wrong_token(csrftoken):
        async with httpx.AsyncClient(
            app=asgi_csrf(hello_world_app, signing_secret=SECRET)
        ) as client:
>           response = await client.post(
                "http://localhost/",
                data={"csrftoken": csrftoken},
                files={"csv": ("data.csv", "blah,foo\n1,2", "text/csv")},
                cookies={"csrftoken": csrftoken[:-1]},
            )

test_asgi_csrf.py:201: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_client.py:1842: in post
    return await self.request(
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_client.py:1514: in request
    request = self.build_request(
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_client.py:356: in build_request
    return Request(
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_models.py:336: in __init__
    headers, stream = encode_request(content, data, files, json)
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_content.py:210: in encode_request
    return encode_multipart_data(data or {}, files, boundary)
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_content.py:155: in encode_multipart_data
    multipart = MultipartStream(data=data, files=files, boundary=boundary)
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_multipart.py:188: in __init__
    self.fields = list(self._iter_fields(data, files))
/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_multipart.py:202: in _iter_fields
    yield FileField(name=name, value=value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <httpx._multipart.FileField object at 0x7ffff5587d00>, name = 'csv'
value = ('data.csv', 'blah,foo\n1,2', 'text/csv')

    def __init__(self, name: str, value: FileTypes) -> None:
        self.name = name
    
        fileobj: FileContent
    
        headers: typing.Dict[str, str] = {}
        content_type: typing.Optional[str] = None
    
        # This large tuple based API largely mirror's requests' API
        # It would be good to think of better APIs for this that we could include in httpx 2.0
        # since variable length tuples (especially of 4 elements) are quite unwieldly
        if isinstance(value, tuple):
            if len(value) == 2:
                # neither the 3rd parameter (content_type) nor the 4th (headers) was included
                filename, fileobj = value  # type: ignore
            elif len(value) == 3:
                filename, fileobj, content_type = value  # type: ignore
            else:
                # all 4 parameters included
                filename, fileobj, content_type, headers = value  # type: ignore
        else:
            filename = Path(str(getattr(value, "name", "upload"))).name
            fileobj = value
    
        if content_type is None:
            content_type = guess_content_type(filename)
    
        has_content_type_header = any("content-type" in key.lower() for key in headers)
        if content_type is not None and not has_content_type_header:
            # note that unlike requests, we ignore the content_type
            # provided in the 3rd tuple element if it is also included in the headers
            # requests does the opposite (it overwrites the header with the 3rd tuple element)
            headers["Content-Type"] = content_type
    
        if isinstance(fileobj, (str, io.StringIO)):
>           raise TypeError(f"Expected bytes or bytes-like object got: {type(fileobj)}")
E           TypeError: Expected bytes or bytes-like object got: <class 'str'>

/gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_multipart.py:111: TypeError
=============================== warnings summary ===============================
../../../gnu/store/hm97w6qgapy8x7i341mhcdn7j3jxfb42-python-pytest-asyncio-0.17.2/lib/python3.9/site-packages/pytest_asyncio/plugin.py:191
  /gnu/store/hm97w6qgapy8x7i341mhcdn7j3jxfb42-python-pytest-asyncio-0.17.2/lib/python3.9/site-packages/pytest_asyncio/plugin.py:191: DeprecationWarning: The 'asyncio_mode' default value will change to 'strict' in future, please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' in pytest configuration file.
    config.issue_config_time_warning(LEGACY_MODE, stacklevel=2)

test_asgi_csrf.py::test_signing_secret_if_none_provided
  /gnu/store/hm97w6qgapy8x7i341mhcdn7j3jxfb42-python-pytest-asyncio-0.17.2/lib/python3.9/site-packages/pytest_asyncio/plugin.py:317: DeprecationWarning: '@pytest.fixture' is applied to <fixture monkeypatch, file=/gnu/store/7frqm5ijy66f81hr8i1j6791k84lds9w-python-pytest-6.2.5/lib/python3.9/site-packages/_pytest/monkeypatch.py, line=29> in 'legacy' mode, please replace it with '@pytest_asyncio.fixture' as a preparation for switching to 'strict' mode (or use 'auto' mode to seamlessly handle all these fixtures as asyncio-driven).
    warnings.warn(

test_asgi_csrf.py::test_multipart_failure_file_comes_before_token
  /gnu/store/993zn04mbdjxvng820d3zmvs0z43j3x5-python-httpx-0.23.0/lib/python3.9/site-packages/httpx/_content.py:204: DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content.
    warnings.warn(message, DeprecationWarning)

-- Docs: https://docs.pytest.org/en/stable/warnings.html
=========================== short test summary info ============================
FAILED test_asgi_csrf.py::test_multipart - TypeError: Expected bytes or bytes...
FAILED test_asgi_csrf.py::test_multipart_failure_wrong_token - TypeError: Exp...
=================== 2 failed, 30 passed, 3 warnings in 0.37s ===================
error: in phase 'check': uncaught exception:
%exception #<&invoke-error program: "/gnu/store/7frqm5ijy66f81hr8i1j6791k84lds9w-python-pytest-6.2.5/bin/pytest" arguments: ("-vv") exit-status: 1 term-signal: #f stop-signal: #f> 
phase `check' failed after 0.7 seconds

I suspect it must have to do with the version of httpx used, which is 0.23.0 in Guix:

Thank you.

Callback for skipping checks

I need more flexibility with regard to skipping checks, for Datasette: simonw/datasette#1377

An optional callback function that gets the scope and specifies if it should be able to skip checks or not would be a great way to do this, and could be backwards-compatible since it could be implemented as a new optional keyword argument.

app = asgi_csrf(
    app,
    signing_secret="secret-goes-here",
    should_protect=lambda scope: True
)

Sign the csrftoken cookie value

https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet#double-submit-cookie has some warnings:

While it’s true that hellokitty.marketing.example.com cannot read cookies or access the DOM from secure.example.com because of the same origin policy, hellokitty.marketing.example.com can write cookies to the parent domain (example.com), and these cookies are then consumed by secure.example.com (secure.example.com has no good way to distinguish which site set the cookie).

And:

b) If an attacker is in the middle, they can usually force a request to the same domain over HTTP. If an application is hosted at https://secure.example.com, even if the cookies are set with the secure flag, a man in the middle can force connections to http://secure.example.com and set (overwrite) any arbitrary cookies (even though the secure flag prevents the attacker from reading those cookies). Even if the HSTS header is set on the server and the browser visiting the site supports HSTS (this would prevent a man in the middle from forcing plaintext HTTP requests) unless the HSTS header is set in a way that includes all subdomains, a man in the middle can simply force a request to a separate subdomain and overwrite cookies similar to 1. In other words, as long as http://hellokitty.marketing.example.com doesn’t force https, then an attacker can overwrite cookies on any example.com subdomain.

Encouraging users to serve over HTTPS can help here, but to ensure no-one sets a known cookie value using an HTTP interception hack I should sign the cookie value with a secret.

I can use https://itsdangerous.palletsprojects.com/ for this.

IndexError thrown on form posts

I'm not quite sure how this part of the code works, so I don't know what I might be doing wrong:

async def replay_receive():

I am using asgi-csrf with a Starlette app, and rendering out the token as a hidden field in forms. In general, csrf checks appear to be passing, however the middleware is throwing an IndexError due to a pop from empty list. The full stack trace is below.

For what it's worth: the form does get submitted and my view is able to process the form as it normally would. So, these exceptions are happening, but it's not clear to me what the real side effect is, or if it might be possible that a proper csrf check is not really happening.

Perhaps there is something obvious I am doing wrong?

console_1        | ERROR:uvicorn.error:Exception in ASGI application
console_1        | Traceback (most recent call last):
console_1        |   File "/usr/local/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
console_1        |     result = await app(self.scope, self.receive, self.send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
console_1        |     return await self.app(scope, receive, send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/asgi_csrf.py", line 143, in app_wrapped_with_csrf
console_1        |     await app(scope, replay_receive, wrapped_send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/applications.py", line 112, in __call__
console_1        |     await self.middleware_stack(scope, receive, send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 181, in __call__
console_1        |     raise exc from None
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/middleware/errors.py", line 159, in __call__
console_1        |     await self.app(scope, receive, _send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/middleware/sessions.py", line 75, in __call__
console_1        |     await self.app(scope, receive, send_wrapper)
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/middleware/authentication.py", line 48, in __call__
console_1        |     await self.app(scope, receive, send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/middleware/base.py", line 26, in __call__
console_1        |     await response(scope, receive, send)
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 224, in __call__
console_1        |     await run_until_first_complete(
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/concurrency.py", line 24, in run_until_first_complete
console_1        |     [task.result() for task in done]
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/concurrency.py", line 24, in <listcomp>
console_1        |     [task.result() for task in done]
console_1        |   File "/usr/local/lib/python3.10/site-packages/starlette/responses.py", line 204, in listen_for_disconnect
console_1        |     message = await receive()
console_1        |   File "/usr/local/lib/python3.10/site-packages/asgi_csrf.py", line 211, in replay_receive
console_1        |     return messages.pop(0)
console_1        | IndexError: pop from empty list

Throws errors if scope["method"] is missing

I got this error:

  File "../site-packages/asgi_csrf.py", line 98, in app_wrapped_with_csrf
    if scope["method"] in {"GET", "HEAD", "OPTIONS", "TRACE"}:
KeyError: 'method'
ERROR:    Application startup failed. Exiting.

Tests fail with httpx==0.16.1

Likely due to encode/httpx@354c4ca.

builder for '/nix/store/667gj5w9jcysm7pad0b4lma5hv4zkggf-python3.7-asgi-csrf-0.7.drv' failed with exit code 2; last 10 log lines:
  ImportError while importing test module '/build/source/test_asgi_csrf.py'.
  Hint: make sure your test modules/packages have valid Python names.
  Traceback:
  test_asgi_csrf.py:7: in <module>
      from httpx._content_streams import MultipartStream
  E   ModuleNotFoundError: No module named 'httpx._content_streams'
  =========================== short test summary info ============================
  ERROR test_asgi_csrf.py
  !!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
  =============================== 1 error in 0.15s ===============================
cannot build derivation '/nix/store/hda4799m3wcb5mfazk2x1s77i0v8qykr-python3.7-datasette-0.46.drv': 1 dependencies couldn't be built

Remove dependency on undocumented httpx classes

The cause of #18 was this test using undocumented API classes from httpx:

asgi-csrf/test_asgi_csrf.py

Lines 232 to 265 in 157647b

class FileFirstMultipartStream(MultipartStream):
def _iter_fields(self, data, files):
for name, value in files.items():
yield FileField(name=name, value=value)
for name, value in data.items():
if isinstance(value, list):
for item in value:
yield DataField(name=name, value=item)
else:
yield DataField(name=name, value=value)
@pytest.mark.asyncio
async def test_multipart_failure_file_comes_before_token(csrftoken):
async with httpx.AsyncClient(
app=asgi_csrf(hello_world_app, signing_secret=SECRET)
) as client:
request = httpx.Request(
url="http://localhost/",
method="POST",
stream=FileFirstMultipartStream(
data={"csrftoken": csrftoken},
files={"csv": ("data.csv", "blah,foo\n1,2", "text/csv")},
boundary=b"boo",
),
headers={"content-type": "multipart/form-data; boundary=boo"},
cookies={"csrftoken": csrftoken},
)
response = await client.send(request)
assert response.status_code == 403
assert (
response.text
== "File encountered before csrftoken - make sure csrftoken is first in the HTML"
)

I filed a httpx feature request to make those stable, but a better solution would be for me to not use them in this way. encode/httpx#1455

Confirm that replay_receive mechanism works correctly

This doesn't look right to me - it looks like it only returns one message rather than looping through them all:

asgi-csrf/asgi_csrf.py

Lines 106 to 109 in 3fc164a

async def replay_receive():
return messages.pop(0)
return dict(parse_qsl(body.decode("utf-8"))), replay_receive

asgi-csrf/asgi_csrf.py

Lines 65 to 70 in 3fc164a

# Consume entire POST body and check for csrftoken field
post_data, replay_receive = await _parse_form_urlencoded(receive)
if secrets.compare_digest(post_data.get(form_input, ""), csrftoken):
# All is good! Forward on the request and replay the body
await app(scope, replay_receive, wrapped_send)
return

Support multipart/form-data upload forms

This is tricky because I'd like to avoid consuming the entire upload body into memory just to check the token. Ideally this would work with streaming the data somehow.

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.