CSRF is only checked when authenticated in DRF?
Asked Answered
I

2

12

TLDR; It seems that my POSTs (to DRF endpoints) are only CSRF protected, if the client has an authenticated session. This is wrong, and leaves the application option to login CSRF attacks. How can I fix this?

I'm starting to build a django rest framework API for a ReactJS frontend, and we want everything, including the authentication, to be handled via API. We are using SessionAuthentication.

If I have an authenticated session, then CSRF works entirely as expected (when auth'd the client should have a CSRF cookie set, and this needs to be paired with the csrfmiddlewaretoken in the POST data).

However, when not authenticated, no POSTs seem to be subject to CSRF checks. Including the (basic) login APIView that has been created. This leaves the site vulnerable to login CSRF exploits.

Does anyone know how to enforce CSRF checks even on unathenticated sessions? and/or how DRF seems to bypass CSRF checks for login?

Below is my rough setup ...

settings.py:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

views.py:

class Login(APIView):

    permission_classes = (permissions.AllowAny,)

    @method_decorator(csrf_protect)  # shouldn't be needed
    def post(self, request, format=None):
        user = authenticate(
            request,
            username=request.POST['username'],
            password=request.POST['password']
        )
        # ... perform login logic ...

    def get(self, request, format=None):
        """
        Client must GET the login to obtain CSRF token
        """
        # Force generation of CSRF token so that it's set in the client
        get_token(request)  
        return Response(None)

urls.py:

urlpatterns = [
    url(r'^login/$', views.Login.as_view(), name='login'),
]

expected behaviour:

login_url = reverse('login')
login_details = {
    'username': self.user.email,
    'password': self.password,
}
client = APIClient(enforce_csrf_checks=True)
# Try to just POST to a CSRF protected view with no CSRF
response = client.post(reverse('login'), login_details)
# response status should be 403 Missing or incorrect CSRF

# GET the login API first to obtain CSRF
client.get(reverse('login'))
login_details['csrfmiddlewaretoken'] = client.cookies.get('csrftoken').value
# Now POST to the login API with the CSRF cookie and CSRF token in the POST data
response = client.post(reverse('login'), login_details)
# response status should now be 200 (and a newly rotated CSRF token delivered)

actual behaviour:

client = APIClient(enforce_csrf_checks=True)
# Try to just to a CSRF protected view with no CSRF
response = client.post(reverse('login'), login_details)
# BROKEN: response status is 200, client is now logged in

# Post to the exact same view again, still with no CSRF
response = client.post(reverse('login'), login_details)
# response status is now 403
# BROKEN: This prooves that this view is protected against CSRF, but ONLY for authenticated sessions.
Indeterminate answered 14/3, 2018 at 10:17 Comment(1)
Great question! Trying to get my head around this... is CSRF protection applicable to an API which is open to the outside? In your example, the attacker can hit the login API via GET to obtain the CSRF token. He can then POST with that CSRF token, so there is no real protection. You could CSRF protect the login view and not give out the token on GET. This is safe, but then you can not login from the "outside" anymore. I guess if login is available via an API that is open to the outside, anyone can login. Maybe for outside access use token authentication instead? Am I missing something?Lobachevsky
L
8

Django REST Framework is disabling CSRF token requirement when using SessionAuthentication and user is not authenticated. This is by design to not mess up other authentication method that don't require CSRF authentication (because they're not based on cookies) and you should ensure by yourself that CSRF is validated on login request and it is mentioned in last paragraph of SessionAuthentication documentation. It is advised to either use non-API login process or ensure that API-based login process is fully protected.

You can check how DRFs SessionAuthentication is enforcing CSRF validation when you are logged in and base your view on that.

Lavoisier answered 14/3, 2018 at 10:44 Comment(3)
So, thanks for that. I've read that doc so many times, no idea how I didn't spot this. Based on the warning "Always use Django's standard login view when creating login pages", I'm guessing the 'way' to solve this, is to just NOT have auth done via the API ... which sucks a bit. Do you know of any examples where what I want (API auth) is achieved in a robust way (i.e. I don't feel like reinventing the CSRF wheel)?Indeterminate
You can do it through API also, but you have to ensure that CSRF validation is active for that particular view.Lavoisier
Yes thank you, I already resolved by using the @method_decorator(csrf_protect) on the dispatch method of my LoginView. All is working and protected.Indeterminate
H
3

You can create a child class of APIView that forces CSRF.

from rest_framework import views

class ForceCRSFAPIView(views.APIView):
    @classmethod
    def as_view(cls, **initkwargs):
        # Force enables CSRF protection.  This is needed for unauthenticated API endpoints
        # because DjangoRestFramework relies on SessionAuthentication for CSRF validation
        view = super().as_view(**initkwargs)
        view.csrf_exempt = False
        return view

Then all you need to do is change your login view to descend from this

class Login(ForceCRSFAPIView)
    # ...
Hieroglyphic answered 7/5, 2020 at 22:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.