Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CSRF is only checked when authenticated in DRF?

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.
like image 539
David Downes Avatar asked Mar 14 '18 10:03

David Downes


People also ask

Which statement is applicable for CSRF validation in REST framework?

As usual CSRF validation will only apply to any session authenticated views. This means CSRF validation will only occur if the client has been logged in by calling login() .

How does CSRF work in Django?

Django has a {% csrf_token %} tag that is implemented to avoid malicious attacks. It generates a token on the server-side when rendering the page and makes sure to cross-check this token for any requests coming back in. If the incoming requests do not contain the token, they are not executed.

What is CSRF and how does Django protect against this attack?

CSRF protection works by checking for a secret in each POST request. This ensures that a malicious user cannot “replay” a form POST to your website and have another logged in user unwittingly submit that form. The malicious user would have to know the secret, which is user specific (using a cookie).

How can I be exempt from CSRF?

You can use the csrf_exempt decorator to disable CSRF protection for a particular view.


2 Answers

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.

like image 53
GwynBleidD Avatar answered Oct 06 '22 01:10

GwynBleidD


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)
    # ...
like image 26
Zags Avatar answered Oct 05 '22 23:10

Zags