Tuesday, July 8, 2014

Cross Origin Resource Sharing (CORS) on server side

As an abstract note: recently I've developed RESTful API for one of my company products. All works excellent while we've used Python client to access. However when trying to query it from a Single Page Application (SPA) Javascript code, I've bumped into security issue. Citing firebug below:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://.... This can be fixed by moving the resource to the same domain or enabling CORS.

There is good Wikipedia article on the subject which gives a global overview of the topic. An in depth guide is here. These two guides will be enough for you to get on, but depending on your environment it make take time to figure out all of the details.

Client side

In three words - "you don't care". Although the second article I've pointed above exposes good details about creating CORS request using raw Javascript, the reality is that most of us use various libraries to do this work. The libraries will do the right thing for you. So most of the time, again, you don't care.

Server side

Server side support boils down to this: Each and Every of your URLs has to support OPTIONS method. This is the flow:
  1. You issue GET/POST request to remote host from your Javascript code
  2. Browser detects that you are targeting host different from the one your script was downloaded from
  3. Browser issues OPTIONS request to the requests URL to find out if and what server allows to do with it. This is called "pre-flight" request and its cached by the browser
  4. If server's answer "satisfies" the browser, it proceeds with executing the original request you've asked for

So again, not only your main URL needs to support OPTIONS, but every URL in your api. I.e. /, /users, /users/1, etc. Note, that accessing URL options should not require any authorization.

OPTIONS response needs include the following headers:

Access-Control-Allow-Origin
Must have. It controls what domains can access your API. Note: not which clients (i.e. browsers), but it contains the list of domains, that if your script was downloaded from one of these domains, browser will allow it to request data from your site. If you are building general purpose API, you can just always put either * there or echo back contents of request's Origin header.
Access-Control-Allow-Methods
Must have. List of methods you allow for this URL. Usually you'll list GET, POST, OPTIONS for collection URLs like /users and GET, PUT, DELETE, OPTIONS for object URLs, i.e. /users/1.
Access-Control-Allow-Headers
Although not required but you'll need it in most cases. It controls which headers you allow the browser to include in request. For example, if you use Basic Auth, you need to put Authorization as a value of this header, otherwise you'll keep getting endless 401 Unauthorized errors.
Another good candidate is Content-Type header - your browser will definitely want to send it when you are doing CRUD requests.
Multiple values can be comma (, ) separated. Header names are case-insensitive.
Access-Control-Allow-Credentials
You'll need this one only if your web service use cookies and you obviously want to allow client to send them. Possible values are either true or false (case matters!). Not setting this header is equivalent to saying Access-Control-Allow-Credentials: false. So you can save some on web traffic here :)
To summarize, your typical response to OPTIONS request will look like this (in case you don't use cookies):
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

And yeah, don't forget to include OPTIONS itself in the list of allowed methods :)

Implementation details

In my environment, REST service sits behind Apache which acts as authentication proxy. Thus I had to split OPTIONS response generation between Apache and my Python code. That is:
  • Access-Control-Allow-Origin is set in Apache, because he is the gate keeper
    Header set Access-Control-Allow-Origin "*"
    
  • Access-Control-Allow-Methods is generated solely by Python code
  • Access-Control-Allow-Headers is set to Content-Type by Python. I did not want to add Authorization header permission in Python, since authentication is done by Apache. So the following line additionally goes in Apache configuration
    Header add Access-Control-Allow-Headers "Authorization"
    

    Note that I'm doing "add" and not "set" in order not to overwrite values already existing in the header (from Python backend).

  • The last trick is that OPTIONS should not be guarded by authentication. Apache's LimitExcept directive comes in handy:
        AuthType Basic
        AuthName "My Realm"
        ....  # Rest of the auth config
        <LimitExcept OPTIONS>
            Require valid-user
        </LimitExcept>
    

Security considerations

After first time reading about CORS I've asked myself: "All this CORS stuff is completely advisory! I've used my API from Python/requests for years without any CORS".

And yes, this is the case indeed. All of this CORS stuff if mainly to protect your from XSS. This is why CORS is blocked by default. To demonstrate the need for it:

  • You go to your bank website, login, obtain session cookie
  • Your bank webpage uses some JS code to poll the server for your account status. Browser automatically sends session cookie together with JS request, so the server lets you in
Now, if CORS was enabled by default, then later on you:
  • Go to some other page that would contain JS code which connects to your bank server and attempts to transfer funds.
  • Browser would happily attach your previous session cookie to the above request and boom - money got stolen. This is why CORS is disabled by default, so these kind of requests would be blocked.

The above scenario also outlines why its not enough to use just session cookie for authorization of CORS requests - that would be easy CSRF attack. In old, "forms world", we had csrf token being part of the form. This token should've match the csrf cookie on the POST request.

On the SPA side of things, there are no classic forms any more, so you need to either use Basic Auth, which is less efficient or session keys:

  • When script logins, set him a session cookie and session key (some string that you can later match against session cookie).
  • With each next request, require your scripts to send this session key as part of the request data, so you can verify it against session cookie

Hope this helps you and I wish you safe coding!

No comments:

Post a Comment