Testing Around CSRF Protection
- 2/21/2015
- ·
- #index
Tests broke when we added blanket CSRF protection. Requests that had slid through easily before became forgeries, “OK” responses became unauthorized, and a cheerful green test suite glowed an angry red.
“Aha!” we thought, “we can just disable protection in our test environment.” And we could, though it would leave our tests proving different code than we would run in production. Clearly less than ideal.
A better way
CSRF protection mechanisms are hardly novel. When we make a request, the response includes a token allowing us to complete some action. If we proceed with that action–submitting a form, say, or sending an XHR–we include the token as verification. Nothing stops us from repeating this manual process in our tests: before testing an action, we can take some other step to acquire a valid CSRF token. Include it in the action, and voilà! What was red is green again.
Consider the process of registering an account. Given a registration form (GET /register
) that can be submitted (POST /register
), we will:
- create a session with the application
- request the form
- extract a CSRF token for the session from the response
- submit the form using the same session and the newly-acquired token
- assert that the results match expectation
For demonstration, we’ll assume that we’re testing a node.js application protected by a CSRF utility like the one bundled in lusca. On the test side, we can use supertest-session to manage persistent test sessions and cheerio for token extraction. These are far from the only tools available for each step of the process, but the general idea should stay the same no matter what libraries (or even languages!) are in use.
Create a session
Since the token we will be retrieving is particular to the active session, we will need to be sure to use the same session for both generating the token and running our tests. For standard, cookie-based sessions, supertest-session gives us an easy wrapper around a persistent cookie jar:
// test/spec.js
var Session = require('supertest-session')({
app: require('../app.js')
});
describe('POST /register', function () {
var session;
beforeEach(function (done) {
session = new Session();
});
// ...
});
Now, all requests made through our session
will appear as the same session to
the application we’re testing.
Extracting the token
In some cases, it might make sense to expose CSRF tokens within an HTTP header
sent back from the server. In this case, extraction is easy–simply check the
header of the ServerResponse
and get on with the test.
In our form-based example, however, the token will be locked away in an
<input>
tag inside the form. There, we can retrieve it using any of a number
of DOM-parsing libraries or any clever scheme we can devise for looking up HTML
attributes. Opting for the former, we can write a quick helper that uses
cheerio
to extract the token value:
var cheerio = require('cheerio')
function extractCsrfToken (res) {
var $ = cheerio.load(res.text);
return $('[name=_csrf]').val();
}
The specific selector used to retrieve the token will vary depending where the
token is store, but–once the lookup completes successfully–we will have the
valid token sent in response to our GET
request:
describe('POST /register', function () {
// ...
var csrfToken;
beforeEach(function (done) {
session.get('/register')
.end(function (err, res) {
if (err) return done(err);
csrfToken = extractCsrfToken(res);
done();
});
});
// ...
});
Writing the test
With session and token in hand, it’s time to make the test pass. The only
trick is that the request must be made through our session
: for the token, we
need only include it in the submitted form data like any other field. Inside a
context with access to session
and csrfToken
, we can now provide valid
responses to our CSRF protection scheme:
// tests....
it('should accept the result', function (done) {
session
.post('/register')
.send({
_csrf: csrfToken,
username: 'foo',
password: 'bar'
})
.expect(201)
.end(done)
});
And there it is! Token sent. Tests pass. Ship it.
Epilogue
It will take longer to run a test suite with twice as many requests. For a large suite with thousands of tests, an extra request just to receive a token may have a significant impact. And depending on the application and the test, blanket CSRF behavior might not even be a necessary detail. It might be reasonable to simply disable protection outright during testing, or to consider a test-only “magic” value acceptable across the test environment. If we depend on representative tests, however, an extra request will be well worth the time.
Example project available on github.