In the last post, we added a user interface with React and CoffeeScript to have something other than raw API endpoints to interact with. In this post we will take a look Cross-Site Request Forgery and how it can be mitigated before it becomes a problem.
As before, the source code for this tutorial series is available on github with the code specifically for this post here. Since the code builds on the previous post, you can grab the current state of the project (where the previous post finished) here.
CSRF Explained
Cross-Site Request Forgery (or CSRF) is a form of attack on users of web applications where a request to the vulnerable website is triggered through non-obvious means. It could be an image tag, an auto-posting form delivered via email or hidden on a malicious site. The end result is that an action is performed by the browser that the user did not intend with unwanted consequences.
James Ward had an great post on securing against CSRF with Play 2.1. While that approach is still very much valid, we will create an alternative implementation for solving the CSRF problem as more recent versions of Play support different mechanisms for filtering web requests.
Why care right now?
The general concern for CSRF attacks stems from when an authorization cookie is present to identify the logged in user. Since Ticket Overlords does not even support the concept of user accounts or sessions yet, why should we care? We care because at the root, a security flaw is just a software flaw with an extra side effect. Naturally, that side effect raises the severity of the flaw from nuisance to potentially critical, but there is rarely a good reason to leave a known flaw in any system when it can be fixed before the severity escalates. In the same vein that our Slick data access layer automatically builds SQL in a safe and secure manner, we can be proactive against another potential threat such that implementing a secure endpoint is easy and trivial to the point that it is as easy to create a safe product as an unsafe one. The lack of exploitability, in this case from the lack of authentication cookies, should never stop you from building software correctly and defensively from the beginning.
Play HTTP Filters
The Play Framework ships with support for multiple HTTP filters, including a filter for CSRF protection. The mechanism used by Play to prevent CSRF is to generate a token, unique for each session, that can be returned with every response in a cookie. When the user makes a protected request, the token sent previously is expected to be present in a form field or optionally an HTTP header. Since we are not using HTML forms, the header is our mechanism of choice.
The filters
helper dependency needs to be added to the existing library
dependencies before the protection mechanisms can be utilized. Open up
build.sbt
and add the filters
dependency somewhere in your
libraryDependencies
sequence.
Reload your activator project and regenerate your eclipse settings if necessary so this dependency is now present on your system.
To specify which filters we want to enable, we are required to create a class
that extends the HttpFilters trait. Create a new class named Filters
directly
under the app
package in your project.
This class uses dependency injection to supply the CSRFFilter
dependency to
the sequence returned by the filters()
method. We will be covering dependency
injection in Play in a later post, but it is worth noting this is the first time
we have directly encountered it.
Now that we have defined our filter trait, we are ready to start locking down
our service endpoints. The most obvious starting point would be the
Orders.create
method. To enable CSRF filtering, it is as straight forward as
adding a CSRFCheck {}
block around the existing Action
. Open up
controllers/Orders.scala
and make the following changes.
At this point we can verify that the Orders.create
endpoint is blocked by the
CSRF filter with a basic curl
command.
This request returns a 403 Forbidden message embedded in a large HTML block.
On the up side, we know our filter is blocking the request. On the down side,
the response is coming back as a formatted HTML document. This is not what we
would desire from an API endpoint. We can create our own custom error response
fairly easily. Create a new file named
app/controllers/responses/CSRFFilterError.scala
To enable this error handler, we need to add an entry to the application.conf
file pointing to this new class.
Once we reload the configuration (just type reload
into the activator prompt),
we can make a curl request and see the expected JSON payload being returned.
Where does the token come from?
If we make any successful curl request, we would expect the CSRF token to be
available. Sending the token as a cookie does not happen automatically, but is
enabled by giving the token cookie a name. Add the following line to
application.conf
to enable sending a CSRF token cookie.
This tells play to make the CSRF token available in a cookie named “CSRF-Token”. By making a new curl request, we can see a token being returned in the response cookie.
Since curl does not handle sessions by default, you can see the CSRF token changing for each curl request.
Now that we have a token coming back, we need to configure the HTTP header that it will expect on the server. We will change it from the default value of “CsrfToken” to “X-CSRF-Token” to be in line with our token cookie’s name.
After reloading our activator instance one more time, we can add both the cookie
and the matching header to our curl request to verify this will actually POST
our request. Note that you may have to change the value for X-CSRF-Token
to a
value supplied by the earlier curl request on your machine.
It appears the request made it through to the application logic, but it turns
out this whole time we had been sending a default Content-Type value of
application/x-www-form-urlencoded
instead of the required application/json
we normally send.
The request was processed by the application logic in the way we expected. We need both the cookie and the header for the CSRF filter to pass. But we forgot the content type this whole time. Why? Why oh why would we forget that?
Shortcutting the Filters
To make up for that whole forgetting the content type, we will now take a couple steps back and try the original failing curl request, but set the content type header.
… What the what?? How could that have possibly worked this whole time? We turned on our CSRF filter, we set the required header and cookie names. It turned out we could bypass it the whole time just by changing the content type? Yes. Play defines a CSRF attack as not being able to perform the following actions:
- Coerce the browser to use other request methods such as PUT and DELETE
- Coerce the browser to post other content types, such as application/json
- Coerce the browser to send new cookies, other than those that the server has already set
- Coerce the browser to set arbitrary headers, other than the normal headers the browser adds to requests
Oddly, the second is just a subset of the fourth item as Content-Type
is
really just an HTTP header. So our endpoint expects an application/json
content type which falls outside of the list of “potentially vulnerable” content
types. In addition, Play supports the ability to bypass all checks if an
X-Requested-With
header is present. There does not even need to be a value
attached to the header.
On the surface these are technically accurate limitations on a standard CSRF
attack. However, in my opinion, they are really just shortcuts around
building a robust system. There is an old saying in the security community:
Shortcuts get stitches… (Ok, nobody has ever said this, but they
should). Just because there is not an obvious attack today with these
properties, it does not mean there will not be in the future. If you can say
with 100% certainty that no version of Flash will ever allow content type
alteration, that no Java applet will ever allow custom headers, and no
experimental implementation of navigator.sendBeacon()
will fail to enforce
origin restrictions please email me.. I would love to know next week’s Powerball
numbers. I would even share the winnings with you 70:30.
Soapbox aside, the default settings shipped with Play are good enough to start with. We can make some minor configuration changes and lock down our API enough that is still safe in the event CSRF attacks add additional attack vectors in the future.
We have added two additional configuration parameters. The first parameter
indicates that that any content type other than *
(which is not a content
type) will trigger a CSRF filter check if it is wrapped with the CSRFFilter
action. This value does not have to be *
, it could be anything other than a
normal content type, feel free to use cyber-banana-vampire
if that makes you
feel more comfortable.
The second parameter disables the magic header bypass shortcut for
X-Requested-With
, since any AJAX calls can easily implement the token to
transfer a cookie value to a header. How easy is it? Open up the
event_list_entry.coffee
component and update it with this code snippet.
Until next time…
We have now secured our ticket ordering endpoint against CSRF attacks. We have taken a more strict approach by altering the default CSRF filter configuration. Overall it usually does not take much extra effort to apply secure coding principles to the development of an application, just a bit of time to research what the default configurations are and any ways to improve on them. In the next post we will add an authentication layer to our API and start supporting user accounts.