Skip to content

How does webargs handle csrf token sent as part of form data? #640

Closed

Description

@simkimsia

I'm using django 3.2 and typically there's a csrfmiddlewaretoken in all form fields.

My use_args are usually pertaining to the specific fields of that form post request

Activity

  1. simkimsia commented on Sep 20, 2021

    @simkimsia
    Author

    Also I'm just a single person business so tide lift is too expensive for me.

    If there's a way I can pay for one off support requests such as answering the question above at a much lower rate, I will be happy to do so.

    I want greater responsiveness but I don't need such a huge level of support as what tide lift offers. I'm happy to write my code I just need questions that will receive guaranteed responses. I don't need super tight turnaround times. 2-3 days or even 2 weeks is fine

  2. sirosen commented on Sep 29, 2021

    @sirosen
    Collaborator

    I'm not aware of the marshmallow projects having any support or contract option other than Tidelift. I'll leave it to others to answer if there's more to say on this.

    webargs has no special handling for csrf-token. It's just "another field on the request" as far as we're concerned.

    Since the CSRF token comes in on the POST body, I would expect it to arrive and need to be handled by your schema or parser, unless the middleware you're using removes it from the parsed body before sending it to your application code.
    Assuming that's right, I see several options:

    1. Add a csrf-token field to all of your schemas
    2. Set your schemas to unknown=EXCLUDE or unknown=INCLUDE (rather than unknown=RAISE, the default)
    3. Use a schema pre_load hook to remove a csrf-token field if one is present
    4. Set your parser to unknown=EXCLUDE or unknown=INCLUDE see doc on setting unknown
    5. Use a parser pre_load hook to remove a csrf-token field if one is present see doc on Parser.pre_load
    6. Use a Django middleware to remove the field before it reaches webargs

    (1-3) are marshmallow features, so the marshmallow docs should give you what you need. And (4) is really just a way in which webargs exposes a marshmallow feature (passing unknown to Schema.load).

    That leaves (5), which is a webargs feature, and (6), which is a Django feature (and therefore beyond the scope of this project).

    For (5), you could do

    class MyParser(DjangoParser):
        def pre_load(self, location_data, *, schema, req, location):
            if location == "form" and "csrf-token" in location_data:
                # `location_data` in the `form` case is a MultiDictProxy of an immutable django QueryDict
                # coerce to a dict to make it mutable
                data = dict(location_data)
                data.pop("csrf-token")
                return data
            return location_data
  3. simkimsia commented on Sep 30, 2021

    @simkimsia
    Author

    thank you, @sirosen

    Are you one of the maintainers here? I did see that you have committed before to this repo.

    I'm currently using a hackish way and conforms to your option 1. But I don't like that as it kinda of implies that this csrf_token is needed by the view (aka the controller endpoint of MVC) or by the model to save records, when actually that's not the case.

    I will give 5 a shot.

    Thank you for your excellent breakdown of my options 1-6. All those to do with webargs i have not heard before and it's awesome that you wrote them as they help me to better learn webargs.

  4. lafrech commented on Sep 30, 2021

    @lafrech
    Member

    Are you one of the maintainers here?

    He is and I think we ought to update AUTHORS.rst.

  5. sirosen commented on Oct 4, 2021

    @sirosen
    Collaborator

    I think we're good to close this. The necessary features to handle this case should all be there.

    If we setup an "Example Gallery" in the docs, like the marshmallow examples page, I'd be happy to put the csrf-removing parser in there... That's the best documentation follow-up I can think of.
    I'm closing this, but I'll open an issue to add an examples page.

  6. simkimsia commented on Oct 5, 2021

    @simkimsia
    Author

    @sirosen

    Apologies, I only have time to try this out now.

    I have a question. I have read https://webargs.readthedocs.io/en/latest/advanced.html#parser-pre-load

    but i am still unsure how to "call" the preload within my view function.

    Here's my views.py

    @use_args(draft_github_repo_total_args, location="form")
    @require_http_methods(["POST"])
    def github_repo_create_post(request, request_args):
        if not request.user.is_authenticated:
            return redirect(
                "%s?next=%s" % (settings.LOGIN_URL, reverse_lazy("repos-create"))
            )
       ...
    

    So where do I call ?

    I have ttried

    webarg_parsers.py

    from webargs.djangoparser import DjangoParser
    
    class MyParser(DjangoParser):
        """
        Note that Parser.pre_load is run after location loading
        but before Schema.load is called.
        It can therefore be called on multiple types of
        mapping objects, including MultiDictProxy,
        depending on what the location loader returns.
        read https://webargs.readthedocs.io/en/latest/advanced.html#parser-pre-load
        """
    
        def pre_load(self, location_data, *, schema, req, location):
            if location == "form" and "csrfmiddlewaretoken" in location_data:
                # `location_data` in the `form` case is a MultiDictProxy of an immutable django QueryDict
                # coerce to a dict to make it mutable
                data = dict(location_data)
                data.pop("csrfmiddlewaretoken")
                return data
            return location_data
    
    from .webarg_parsers import MyParser
    parser = MyParser()
    
    # https://webargs.readthedocs.io/en/latest/framework_support.html#django
    @parser.pre_load
    @use_args(draft_github_repo_total_args, location="form")
    @require_http_methods(["POST"])
    def github_repo_create_post(request, request_args):
    

    Then i get TypeError: pre_load() missing 3 required keyword-only arguments: 'schema', 'req', and 'location'

    So I guess I am supposed to instantiate inside the view function ?I can roughly guess what i need to use to fill req and location but what about schema?

  7. sirosen commented on Oct 5, 2021

    @sirosen
    Collaborator

    Parser pre_load gets called as part of the parser's use_args, use_kwargs, or parse invocation.

    You haven't shown which use_args you're using, but I'm guessing that you're doing from webargs.djangoparser import use_args. That's just an alias for the default parser's use_args method.

    Use the use_args decorator from the parser which defines the pre_load call instead:

    parser = MyParser()
    
    @parser.use_args(...)  # <--- this, use_args from the parser, not the default parser
    def view_func(...):
        ...
  8. simkimsia commented on Oct 6, 2021

    @simkimsia
    Author

    Works. Thank you @sirosen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions