I'm implementing a stateless API and my organization says that I need to protect against CSRF attacks.
I found this guy's solution online and decided to try implementing a client-side only approach: http://blog.jdriven.com/2014/10/stateless-spring-security-part-1-stateless-csrf-protection/
Here's what the site says to do for a stateless solution (in case the site goes down):
- CLIENT-SIDE GENERATED CSRF-TOKENS. Have the clients generate and send the same unique secret value in both a Cookie and a custom HTTP header. Considering a website is only allowed to read/write a Cookie for its own domain, only the real site can send the same value in both headers. Using this approach all your server has to do is check if both values are equal, on a stateless per request basis!
Unfortunately it's not working. My header value never matches my cookie value, and in some cases it seems like my header is just one request behind matching the cookie value.
Here's my Angular code:
app.config(['$httpProvider', function ($httpProvider) {
//fancy random token
function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e16]+1e16).replace(/[01]/g,b)};
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
$httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';
$httpProvider.interceptors.push(function () {
return {
'request': function (config) {
document.cookie = 'CSRF-TOKEN=' + b();
return config
}
};
});
}]);
Here are some examples of the CSRF values that are being sent.
CSRF-TOKEN=d25cf03a985d575ad48a863eac91467666
X-CSRF-TOKEN:fa1f165df8b27195a90f5e7841108f4e42
CSRF-TOKEN=d25cf03a985d575ad48a863eac91467666
X-CSRF-TOKEN:fa1f165df8b27195a90f5e7841108f4e42
CSRF-TOKEN=9c8dd46ed06c250b707ac0cb80a08a23ac
X-CSRF-TOKEN:d25cf03a985d575ad48a863eac91467666
CSRF-TOKEN=eb407a0303c21173fe4d0ae03c97eaea6d
X-CSRF-TOKEN:0cf066bf83e50b5c74cb932ab8a47c94e8
CSRF-TOKEN=506355a940a2ac5b48f363712b34570d73
X-CSRF-TOKEN:eb407a0303c21173fe4d0ae03c97eaea6d
What's going on here? I feel like I'm doing everything that is in that guy's solution, but am ending up with weird results.
I ended up not sending a random token that was created by the client side with each request. I couldn't get it to work.
So here's how I solved my problem (kind of):
(1) On each request (including the first one), I send back from my API a cookie in the response headers with the name "XSRF-TOKEN" and a randomized value associated with it. This is the name AngularJS is looking for by default when using its CSRF protection.
(2) What happens in the request following having received that token, is AngularJS uses that token's value in sending a cookie in the request headers called "XSRF-TOKEN" and also a header called "X-XSRF-TOKEN" with that token's value as well.
So my API is handling randomizing the XSRF token, and my application is still stateless. I'm using Web API, and am using a global filter to handle this XSRF token creation. Below is my code (in C#) for doing this. I no longer have any code handling this in the UI (as it doesn't appear it to be needed):
public class ValidateAntiForgeryToken : ActionFilterAttribute
{
private const string XsrfCookieName = "XSRF-TOKEN";
private const string XsrfHeaderName = "X-XSRF-TOKEN";
private const string CsrfTokenSalt = "RANDOM SALT";
public override void OnActionExecuting(HttpActionContext filterContext)
{
string requestMethod = filterContext.Request.Method.Method;
Boolean isValid = true;
if (requestMethod != "GET")
{
var headerToken = filterContext.Request.Headers.Where(x => x.Key.Equals(XsrfHeaderName, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Value).SelectMany(x => x).FirstOrDefault();
var cookieToken = filterContext.Request.Headers.GetCookies().Select(x => x[XsrfCookieName]).FirstOrDefault();
// check for missing cookie or header
if (cookieToken == null || headerToken == null)
{
isValid = false;
}
// ensure that the cookie matches the header
if (isValid && !String.Equals(headerToken, cookieToken.Value, StringComparison.OrdinalIgnoreCase))
{
isValid = false;
}
if (!isValid)
{
filterContext.Response = filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
filterContext.Response.ReasonPhrase = "Unauthorized to make that request.";
return;
}
}
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
string textToHash = RandomStringGeneration();
string cookieText = HashService.HashText(textToHash, CsrfTokenSalt);
var cookie = new CookieHeaderValue(XsrfCookieName, HttpUtility.UrlEncode(cookieText));
/* don't use this flag if you're not using HTTPS */
cookie.Secure = true;
cookie.HttpOnly = false; // javascript needs to be able to get this in order to pass it back in the headers in the next request
/* if you have different environments on the same domain (which I did in one application using this code) make sure you set the path to be ApplicationPath of the request. Case sensitivity does matter in Chrome and IE, so be wary of that. */
cookie.Path = "/";
actionExecutedContext.Response.Headers.AddCookies(new[] { cookie });
base.OnActionExecuted(actionExecutedContext);
}
}
Here's my HashService.HashText() code:
public class HashService
{
public static string HashText(string text, string salt)
{
SHA512Managed hashString = new SHA512Managed();
byte[] textWithSaltBytes = Encoding.UTF8.GetBytes(string.Concat(text, salt));
byte[] hashedBytes = hashString.ComputeHash(textWithSaltBytes);
hashString.Clear();
return Convert.ToBase64String(hashedBytes);
}
}
Hopefully this helps someone in the future in stateless apps. Unfortunately the token that's sent back is only compared against itself in the cookie value and header value. That's the only way I'm able to verify it right now (which I feel is pretty safe). I might create a whole new table for XSRF protection, and use that for verifying that a token is indeed the one a user should be using. This is the only way I can wrap my mind around keeping my API stateless.
I stumbled upon this solution after reading the $http documentation for AngularJS, which dictates:
Cross Site Request Forgery (XSRF) Protection: XSRF is a technique by which an unauthorized site can gain your user's private data. Angular provides a mechanism to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie (by default, XSRF-TOKEN) and sets it as an HTTP header (X-XSRF-TOKEN). Since only JavaScript that runs on your domain could read the cookie, your server can be assured that the XHR came from JavaScript running on your domain. The header will not be set for cross-domain requests.
To take advantage of this, your server needs to set a token in a JavaScript readable session cookie called XSRF-TOKEN on the first HTTP GET request. On subsequent XHR requests the server can verify that the cookie matches X-XSRF-TOKEN HTTP header, and therefore be sure that only JavaScript running on your domain could have sent the request. The token must be unique for each user and must be verifiable by the server (to prevent the JavaScript from making up its own tokens). We recommend that the token is a digest of your site's authentication cookie with a salt for added security.
The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, or the per-request config object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With