How Not To Prevent CSRF in a RESTful Service

Originally published here: https://www.optiv.com/blog/how-not-to-prevent-csrf-in-a-restful-service

On a recent client engagement, I was given a RESTful service as a target of an application penetration test. Its target consumer was both a web app and a mobile app. Like many RESTful services, it was also stateless and vulnerable to Cross Site Request Forgery (CSRF) out of the gate.

The client’s developers got right on fixing the CSRF vulnerability. Their second iteration of the service was quite complicated with a new anti-CSRF security strategy: hash-based message authentication code (HMAC) tokens in custom HTTP request headers. Thanks to teammate Johnny Yu’s example burp extension to edit HTTP headers written in python, whipping up a burp extension to build these custom HMAC headers was quick and easy. The following is an example request with the new HMAC headers:

GET /api/some/endpoint?id=0 HTTP/1.1
Host: [redacted]
time: 2015–03–19T16:45:08Z
userid: jsmith1
hmac: wiRs8BfHGWqpNBFXieVxwDQ9%2Bbbsv0cuiwpNDFiUW40%3D
[…snip…]

The HMAC in this example is simply the relative URL with query string parameters for GET requests (POST requests included the JavaScript object notation (JSON) encoded message body) concatenated with the timestamp and user ID of the request (“time” and “userid” above). The application generates a keyed SHA256 hash from the concatenated string and places the result into the “HMAC” header. The following python snippet demonstrates this process:

import hashlib
import hmac
import base64
key = ‘ufS01i0TK2DfKe4uiduBSyEjX+Os62ojBGk9KxXN5qc=’
url = ‘/api/some/endpoint?id=0’
date = ‘2015–03–19T16:45:08Z’
userid = ‘jsmith1’
expected = ‘wiRs8BfHGWqpNBFXieVxwDQ9+bbsv0cuiwpNDFiUW40=’
calculated = base64.b64encode(hmac.new(base64.b64decode(key), url + date + userid, hashlib.sha256).digest())
print url
print date
print userid
print “Calculated: %s” % calculated
print “Expected: %s” % expected
if calculated == expected:
print “Successful Match”

At first glance this appears like a decent solution. After all, the server can generate the same HMAC by disassembling each request that comes in, concatenating the various pieces together, and applying the same key to the SHA256 hash. To make really sure these requests couldn’t be replayed, the client’s developers setup a very short time to live (30 seconds) timeout policy to these messages, enforced on the server. The messages themselves appear to be unpredictable due to these custom HMAC headers, which can be easily set on any requests initiating from a mobile app consumer and can also still be set in the browser when using XmlHttpRequest in JavaScript.

The following is an excerpt of JavaScript similar to what was in the consumer web app:

function generateHmac(data) {
var hash = CryptoJS.HmacSHA256(data, CryptoJS.enc.Base64.parse(hmacKey));
hash = hash.toString(CryptoJS.enc.Base64);
console.log(hash);
return hash;
}
function ajaxGetReq(uri) {
var headers = authHeaders;
var date = new Date();
headers.hmacTime = date;
headers.hmac = generateHmac(uri+date+userid);
$.ajax({
type : “GET”,
url : domain + uri,
headers : headers,
async : true,
dataType : ‘json’,
[…snip…]

Note the highlighted reference to a global variable containing an HMAC key. In this case, the client shared the key across all service consumers, from the web app to the mobile app. Since this key was shared, there are plenty of opportunities to grab a copy of it: from the browser, the raw HTTP server response, the mobile app’s configuration or local storage, etc. If a web app can generate these HMACs, then a third-party web app can also forge them as part of a dynamically created CSRF attack.

The moral of the story is that it’s risky business to build your own security features. This problem is draped in new-ish technology (RESTful services), but at the end of the day it’s the same problem crypto implementers have had forever: where is the key and who has a copy of it? Improperly implemented HMAC header authentication is not an automatic talisman against CSRF.

If you or your team is building something similar, have you considered all the angles?