• BLOG

  • Legitimate Cross-Site Request... a CORS and CSRF story (EN)

    jeu. 20 avril 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    CORS and CSRF are two mechanisms built for Web Application protection. However, most of their implementations are incompatible with each other, and I haven't seen any documentation on this problem.

    This is my journey to build a secure application, protected toward both CSRF and CORS attack.

    Firstly, I'll explain what is CORS and CSRF, and how django implement protections against it, then, I'll present the solution I designed to enable a legitimate Web Application performing Cross-Site Request toward a Web API, while protecting the Web API against CSRF and CORS.

    Some elements of context

    Web Application, as developed today, are awesome! You can develop a whole application with a real separation between the logic and the interface.

    In my example, we will use the following use case:

    • A Web API, developed in a django application, that handles the whole logic of the application (login, registration, CRUD, ...)
    • A Web UI, developed with any javascript swag-work (react, angular, vue.js, ...)

    Both stacks are hosted on different servers, different hostnames, let say https://awesome_web_api/ and https://awesome_web_ui/.

    Users navigate to https://awesome_web_ui/ to use the application, and AJAX queries are performed toward https://awesome_web_api/ to interact with the service.

    Thus far, nothing fancy, I don't think there is any exotic requirements here... however, if you want to secure this flow... it can be really painful! Let's see why.

    What is CORS ?

    CORS stands for Cross-Origin Resource Sharing. The main problem behind it, is that you don't want any website to be able to let their user performs AJAX queries toward your website.

    Let's take an example. Imagine https://awesome_web_api/ has an API endpoint such as, if an authenticated user goes to https://awesome_web_api/users/me he can retrieve details about him, for example:

    GET /users/me
    
    200 OK
    
    {
      "name":"Blusky",
      "email":"blusky@awesome.io",
      "score":42
    }
    

    Now, your main competitor have a website called https://awful_evil/, and wants to steal your users. If on his website he performs an AJAX request toward https://awesome_web_api/, there is a security mechanism implemented inside the web browser that perform a preliminary request toward https://awesome_web_api/ with the following content :

    OPTION /users/me
    Origin: https://awful_evil/
    

    So now, the server should respond that request. If (and only if) the response header contains Access-Control-Allow-Origin: https://awful_evil/, the original request will then be sent. If not, the web browser will trigger a security warning in console logs.

    Obviously, default settings will forbid any Origin, but in our case, we want to add https://awesome_web_ui/ to allowed origins list for https://awesome_web_api/.

    There are also some other subtleties that will have some meanings later:

    • Access-Control-Allow-Header let you specify which headers can be read by Javascript
    • Access-Control-Allow-Credentials let you keep a cookie jar, in order to keep a session alive

    What is CSRF ?

    CSRF stands for Cross-Site Request Forgery. With CORS, we saw how to prevent AJAX queries to be performed from other website. However, there is other ways than AJAX to perform requests.

    In this example, imagine https://awesome_web_api/ got an API endpoint such as, if an authenticated user goes to https://awesome_web_api/messages/delete he can perform operations, for example:

    POST /messages/delete
    
    message_id=1
    
    200 OK
    
    {
      "status":"ok"
    }
    

    For example, you can create the following form on https://awful_evil/:

    <form method='POST' action='https://awesome_web_api/messages/delete' id="csrf-form">
      <input type='hidden' name='message_id' value='1'>
    </form>
    <script>
      document.getElementById("csrf-form").submit()
    </script>
    

    When a user navigates to https://awful_evil/, this form will automatically be submitted, and your first message will be deleted. The main problem is, the attacker can "predict" the whole content of your form. Sadly, for such mechanism, CORS are not protecting your website, and no "built-in" mechanism exists for this. You have to implement what is called a CSRF token.

    There is are renowned ways to do this:

    • For each form created on your website, generate a random token that will be added in a hidden field in the view. When the form is submitted, the application must check that given token corresponds to submitted form.
    • If no CSRF cookie exists, generate one, for CSRF protected end points, a CSRF header must be set, and the value of the CSRF header is the same as the CSRF cookie, the request is considered as legitimate.

    The first method cannot be used in our case, because https://awesome_web_ui/ does not use HTML forms, but performs AJAX requests. Therefore, the second choice will be used... and that's fine, it's the default django behavior.

    How to implement it with django ?

    In our example, let's say our django project is called awesome, and our application is called awesomeapp.

    CSRF are handled by default, for all authenticated POST request, with the django.middleware.csrf.CsrfViewMiddleware middleware.

    CORS whitelisting is not settable in vanilla django, we will use the django-cors-headers dependency that can be installed with

    pip install django-cors-headers
    

    Then, the following configuration enables the protection inside the application:

    /awesome/awesome/settings.py

    CORS_ORIGIN_WHITELIST = ('http://awesome_web_ui/',)
    CORS_ALLOW_CREDENTIALS = True
    
    INSTALLED_APPS = [
        ...
        'corsheaders',
        'myapp',
        ...
    ]
    
    MIDDLEWARE = [
        ...
        'corsheaders.middleware.CorsMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        ...
    ]
    

    So now that everything's up and ready, why the fuss ?

    That's exactly where... things do not seem to work as expected. Here's why.

    So in http://awesome_web_ui/, I can perform operations on the http://awesome_web_api, and the Web API server allows those requests. Let's re-use our previous example and imagine that an authenticated user able to list messages wants to delete the message message_id=1

    The web browser will send the following request to the API:

    POST /messages/delete
    
    message_id=1
    

    But beforehand, the browser will check the CORS and send the following request

    OPTION /messages/delete
    
    Origin: https://awesome_web_ui/
    

    As awesome_web_ui is whitelisted, the server give a 200 OK response, so the POST request is sent.

    However, the following response is given:

    403 Forbidden
    
    CSRF verification failed
    

    And... indeed, we did not send any CSRF token, so that response is normal... We need to send the CSRF token to prove that our request is legitimate.

    But... Where is the CSRF Token ? In the csrfcookie ! Let's check csrfcookie !

    But it's not possible :-( Indeed, on the https://awesome_web_ui/ context, this cookie is never set, and we cannot execute Javascript code on the http://awesome_web_api context from https://awesome_web_ui/ in order to retrieve it.

    So how ?

    Solution

    I did not see ANY documentation on this subject. If there is a standard for such situation, I would love to hear it.

    What I did, is create a new header in the response of each request, that contains the CSRF token value. This way, the CSRF Token is on two different places: the cookie jar, and this new header (called X-CSRFToken). I needed to add Access-Control-Allow-Header: X-CSRFToken in request's response, so CORS allows Javascript to read this header.

    This way, our previous request:

    POST /messages/delete
    
    message_id=1
    

    Send the following response:

    403 Forbidden
    X-CSRFToken: 65az231fq65az4
    
    CSRF verification failed
    

    So now, the Javascript can retrieve the CSRF Token value, and generate the following request:

    POST /messages/delete
    X-CSRFToken: 65az231fq65az4
    
    message_id=1
    

    That will trigger the following response:

    200 OK
    
    {
      "status":"ok"
    }
    

    Wow.... finally!

    Django implementation

    In order to implement that in a Django application, we need to create a middleware that will retrieve CSRF Token, and add it to requests:

    /awesome/awesomeapi/utils.py

    def csrf_header_middleware(get_response):
        def middleware(request):
            csrf_token = ""
            if "HTTP_COOKIE" in request.META:
                http_cookies = request.META['HTTP_COOKIE'].split("; ")
                for http_cookie in http_cookies:
                    if http_cookie.startswith("csrftoken="):
                        csrf_token = http_cookie.split("=")[1]
            response = get_response(request)
            if 'CSRF_COOKIE' in request.META:
                csrf_token = request.META['CSRF_COOKIE']
            response.__setitem__("X-CSRFToken", csrf_token)
            return response
        return middleware
    

    We need to add this middleware just before the Django CsrfViewMiddleware:

    /awesome/awesome/settings.py

    CORS_EXPOSE_HEADERS = ['X-CSRFToken']
    
    MIDDLEWARE = [
        ...
        'awesome.awesomeapi.utils.csrf_header_middleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        ...
    ]
    

    That's all folks :-)

  • Comments