This is a simple demo webapp for the Electrode Stateless CSRF in Hapi.
It consists of three parts:
- A NodeJS server using electrode-server and integrated with the Hapi CSRF plugin.
- A JavaScript file for making AJAX GET/POST requests with CSRF protection from the browser.
- A very simple HTML page for interaction
The demo expects the following domain name aliases setup for your localhost.
dev.mysite.com
- Your trusted sitecdn.mycdn.com
- Your trusted CDN servers (for serving the JavaScript file)dev.badsite.com
- The malicious site
If you are on *nix and you can sudo
, then you can edit your /etc/hosts
file and add the following lines to simulate them:
127.0.0.1 dev.mysite.com
127.0.0.1 cdn.mycdn.com
127.0.0.1 dev.badsite.com
To start the demo, run npm run demo
.
You should see something similar to the following:
$ npm run demo
> [email protected] demo /dev/electrode-csrf-jwt
> node demo/server
staticPaths Plugin: static files route prefix "" path prefix "demo" at routes js,images,html
Hapi.js server running at http://L-SB8H1M2G8W-M:3000
Now point your browser to http://dev.mysite.com:3000
, open the console in dev tools, and click the buttons to try the GET or POST AJAX requests.
You can also point your browser to http://dev.badsite.com:3000
and observe the browser refusing to let the POST request through because of the HTTP header not allowed error.
To test multi tab, open another browser tab to http://dev.mysite.com:3000
, make sure "use localStorage" is on
for both, and click the POST button alternatively in the two tabs. Observe that the requests will go through successfully.
Other than the basic stateless CSRF demonstration, the following are also implemented:
- Making first POST call work
- Multi browser tab support
In order to demo some features, the NodeJS server disabled these security settings:
- Turn off HTTP Only flag in the token cookie to allow the code to inspect and show it.
- Enable CORS and override Hapi's internal HTTP OPTIONS for the routes to avoid the browser refusing requests from
badsite.com
due to CORS check, but then refuse it due to HTTP header not allowed check.
Make sure you don't do these in your app. Please do not copy and paste code from the demo into production app.
To keep your application as secured as possible, it's recommended that your JavaScript making the AJAX requests keep the CSRF header token in an internal variable.
There are two issues with doing that however:
- Your first request must be GET to prime the tokens.
- Your application doesn't work across browser tabs.
If you implement retry, then both issues should be taken care of automatically.
To solve the issues without relying on retry, a trusted and secured channel for sharing the header token is required. Given this happens on the user's browser, it's tricky. Ultimately we have to go by the standards but still depend on the actual implemention in different browsers.
In this demo, the header token is stored into window.localStorage
in the following keys:
initial-csrf-header-token
- store the header token that's dropped by the first page for use if first request has to be POST.csrf-header-token
- store the normal header token acquired after each request.
If you want to limit the routes that allow first POST calls, then you should do the following:
- In your request, set the HTTP header
x-csrf-first-post
to"1"
. - On the server side, set the route config option
allowFirstPost
totrue
(Hapi only for now)
Other routes that doesn't have the allowFirstPost
set to true would refuse such requests.
Regardless of how secured window.localStorage
is, by using it, you expand the attackable area of your application. So you should evaluate the implications before using it.
Another option is to open an iframe
and use message passing to store the header token into that in order to shared it across tabs. Of course there are other potential attack vectors for that as well.
However, the question is, what is the consequence even if some malicious code found a way to access your application's window.localStorage
? In the end, these two restrictions are still the most critical:
- cross site scripts can't access cookies, especially HTTP only ones.
- cross site scripts can't set HTTP headers