Sat 06 Jan 2007

Defending against CSRF in AllegroServe

Any web application that uses an authentication token stored in a cookie — that is, a large portion — is potentially vulnerable to Cross Site Request Forgery attacks. One of your users visits another site, A; that site uses script fetching or XHR to ask the user's browser to request some data from your site, B. That data is then manipulated through Javascript in the page generated by site A (perhaps being sent back to a malicious entity). The user isn't aware of the request to site B (it happens in the background), but because their authentication tokens are still in the browser, and are (incorrectly, but usually) passed along with the request, site A has access to their data.

A more thorough treatment of this, and the solution I'm going to demonstrate, is on Ajaxian and on Joe Walker's blog.

Read them? Good. Let's implement double submission of cookies.

Let's assume your page uses AllegroServe's function authorizers to check the contents of a cookie. You store your function authorizer in *cookie-authorizer*, and publish your pages like this:
(net.aserve:publish :path "/secret/" 
:function #'make-secret-page
:authorizer (list *cookie-authorizer*))
Let's define a function to check double-submission. The cookie token is passed in a parameter named token. In the cookie itself, it is named digest, and it probably looks a little like this: 195876e324e524a5d3c5eec72cdb5498ad3861f8.
(defun check-double-submission (req ent auth)
"Returns :deny if req does not contain a URL param,
token, matching the cookie."
(declare (ignore ent auth))
(let ((cookie (cdr (assoc "digest" (get-cookie-values req)
:test #'string=)))
(token (request-query-value "token" req :test #'string=)))
(if (and cookie token (string= cookie token))
t
;; Could just return nil.
:deny)))

(defparameter *double-submission-authorizer*
(make-instance 'net.aserve:function-authorizer
:function #'check-double-submission))
Now we need to ensure that our own requests send the parameter. When generating HTML it's easy:

(let ((the-url
(format nil "/secret/do-x?token=~A"
(net.aserve:uriencode-string
(or
(cdr
(assoc "digest"
(net.aserve:get-cookie-values req)
:test #'string=))
"")))))
;; Use the-url.

)
It's even easier if your requests are made through Javascript; just adjust your callURI or equivalent function to alter its destination URL to include the cookie value, by calling addToken on the URL:
function getCookieToken() {
var cookies = document.cookie.split(';');
var cookieLength = cookies.length;
for (var i=0; i<cookieLength; ++i) {
var cookie = cookies[i];
// Remove leading spaces.
while (cookie.charAt[0] == ' ') {
cookie = cookie.substring(1);
}

// Found?
var pos = cookie.indexOf('digest=');
// Sometimes it's at index 1...!
if (pos != -1) {
return cookie.substring('digest='.length + pos);
}
}
}

function addToken(url) {
var token = getCookieToken();
if (token) {
return url + ((url.indexOf('?') == -1) ? '?' : '&') +
'token=' + token;
}
else {
return url;
}
}

// Substitute into your AJAX functions…
function callURI(url, …) {

re.open(addToken(url));

}
The getCookieToken function extracts the appropriate field from the cookie; addToken appends it to the URL. Now, everywhere you use callURI, a token parameter will be appended. You can do this manually if you regularly make off-site requests and don't want authentication tokens going with them, or just want more control.

All that's left now is to re-publish your pages:
(net.aserve:publish :path "/secret/" 
:function #'make-secret-page
:authorizer
(list *cookie-authorizer*
*double-submission-authorizer*))
The request will only reach make-secret-page if the cookie is valid, passing *cookie-authorizer*'s tests, and is accompanied by a valid double submission. Tada!

Posted at 2007-01-06 15:47:16 by RichardLink to Defending against …
Comments, trackbacks.

Google
Web holygoat.co.uk
  • richard is: