CSRF Protection
Requirements
- Stateless server. The solution should not require extra server side state. This prevents us from using server side sessions, or using a global dictionary to lookup tokens
- Static HTML. Several security solutions require the manipulation of HTML on the way to the client. Given our desire to support multiple back-ends, this is not acceptable.
- Should prevent Basic CSRF - So cookies alone are not sufficient to re-authenticate a client
- Should prevent Login CSRF - So the identification token must be provided to the client prior to the initial authentication request. Since the server can't store information on auth-state (see above), this implies that 2 different types tokens must be used, one for pre-auth and one for post-auth
- Should prevent Session Fixation - Session fixation is a problem for any site that allows URL re-writing, or has an XSS flaw, we just need to check that these don't apply to us!
Design
How it worked previously:
- All of the pages already call /register/userinfo/ first thing to ensure that the user is logged in.
- logged in users presently have a signed cookie that identifies them.
- /register/userinfo/ currently returns some information if the user is logged in. Otherwise, it returns a 401.
- /register/login/username sets the authentication cookie.
- /register/logout/ deletes the authentication cookie
- all authenticated URLs verify the cookie and use that to get the username.
New additional behavior:
- In server.js: when the first remote call to the server is made the client first generates an unpredictable token, it places this value into a cookie called "Domain-Token".
- On every call it recalls the value (or reads the token from the cookie), and places it in a header called "Domain-Token".
For example:
var token = dojo.cookie("Domain-Token"); if (!token) { token = server._randomPassword(); dojo.cookie("Domain-Token", token); } xhr.setRequestHeader("Domain-Token", token);
Our implementation of _randomPassword() is as follows:
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; pass = ""; for (var x = 0; x < 16; x++) { var charIndex = Math.floor(Math.random() * chars.length); pass += chars.charAt(charIndex); } return pass;
- In framework.py: On receipt of an XHR call we extract the Domain-Token header and check the value is the same as the "Domain-Token" cookie, throwing an exception if they do not match
query_token = request.cookies.get("Domain-Token") reply_token = environ.get("HTTP_DOMAIN_TOKEN") if query_token is None or reply_token != query_token: response.error("401 Not Authorized", "CSRF Token Error")
- In addition we introduced a skip_token_check parameter to the expose annotation to allow us to serve static resources without CSRF checks and a BespinTestApp that extended webtest.TestApp to inject a Header/Cookie as we expect the client to do.
- Finally we expose this token to the server to allow it's use in message routing.
self.session_token = environ.get("HTTP_DOMAIN_TOKEN")