Cameron Hotchkies

Categories

  • Coding

Tags

  • csrf
  • play
  • scala
  • security
  • tutorial

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.

libraryDependencies ++= Seq(
  specs2 % Test,
  cache,
  ws,
  filters,
  "org.webjars" % "jquery" % "2.1.4",
  "com.typesafe.play" %% "play-slick-evolutions" % "1.0.0",
  "com.h2database" % "h2" % "1.4.187",
  "com.typesafe.play" %% "play-slick" % "1.0.0",
  "org.webjars" % "requirejs" % "2.1.18",
  "org.webjars" % "react" % "0.13.3"
)

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.

import play.api.http.HttpFilters
import play.filters.csrf.CSRFFilter
import javax.inject.Inject

class Filters @Inject() (csrfFilter: CSRFFilter) extends HttpFilters {
  def filters = Seq(csrfFilter)
}

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.

// ... <snip> ...

import play.filters.csrf._

// ... <snip> ...

def create = CSRFCheck {
  Action.async(parse.json) { request =>

    // ... <snip> ...

  }
}

At this point we can verify that the Orders.create endpoint is blocked by the CSRF filter with a basic curl command.

$ curl -i -w '\n' http://localhost:9000/orders/ \
  -d '{"ticketBlockID":1,"customerName":"Non Friendly",'\
  '"customerEmail":"attaxor@semisafe.com", "ticketQuantity":9876}'

This request returns a 403 Forbidden message embedded in a large HTML block.

HTTP/1.1 403 Forbidden
Set-Cookie: PLAY_SESSION=; Max-Age=-86400; Expires=Fri, 24 Jul 2015 18:35:43 GMT; Path=/; HTTPOnly
Content-Type: text/html; charset=utf-8
Date: Sat, 25 Jul 2015 18:35:43 GMT
Content-Length: 2111

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Unauthorized</title>

        ... more HTML ...

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

package controllers.responses

import play.api.mvc._
import play.api.mvc.Results.Forbidden
import play.api.http.Status
import play.api.libs.json.Json
import scala.concurrent.Future
import play.filters.csrf.CSRF.ErrorHandler

class CSRFFilterError extends ErrorHandler {
  def handle(req: RequestHeader, msg: String): Future[Result] = {
    val response = ErrorResponse(Status.FORBIDDEN, msg)
    val result = Forbidden(Json.toJson(response))

    Future.successful(result)
  }
}

To enable this error handler, we need to add an entry to the application.conf file pointing to this new class.

# ... <snip> ...

# CSRF Configuration
play.filters.csrf.errorHandler=controllers.responses.CSRFFilterError

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.

$ curl -w '\n' http://localhost:9000/orders/ \
  -d '{"ticketBlockID":1,"customerName":"Non Friendly",'\
'"customerEmail":"attaxor@semisafe.com", "ticketQuantity":9876}'

{"result":"ko","response":null,"error":{"status":403,"message":"No CSRF token found in headers"}}

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.

# ... <snip> ...

# CSRF Configuration
play.filters.csrf.errorHandler=controllers.responses.CSRFFilterError
play.filters.csrf.cookie.name = "CSRF-Token"

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.

$ curl -i -w '\n' http://localhost:9000/events/

HTTP/1.1 200 OK
Set-Cookie: CSRF-Token=681dcdaf1a012296e4fc406a25d65e361c6033f4-1438009476621-d0bcb1b34690996164987a58; Path=/
Content-Type: application/json; charset=utf-8
Date: Mon, 27 Jul 2015 15:04:36 GMT
Content-Length: 322

{"result":"ok","response":[{"id":1,"name":"Kojella","start":1397746800000,"end":1397973600000,"address":"123 Paper St.","city":"Palm Desert","state":"CA","country":"US"},{"id":2,"name":"Austin Satay Limits","start":1443801600000,"end":1444017600000,"address":"456 Skewer Ave","city":"Austin","state":"TX","country":"US"}]}

Since curl does not handle sessions by default, you can see the CSRF token changing for each curl request.

$ curl -i -w '\n' http://localhost:9000/events/
HTTP/1.1 200 OK
Set-Cookie: CSRF-Token=997917baae485a6382e2092e03e39e5ba848ba3b-1438014702768-7c407d745322c7cd20e87979; Path=/
Content-Type: application/json; charset=utf-8
Date: Mon, 27 Jul 2015 16:31:42 GMT
Content-Length: 322

{"result":"ok","response":[{"id":1,"name":"Kojella","start":1397746800000,"end":1397973600000,"address":"123 Paper St.","city":"Palm Desert","state":"CA","country":"US"},{"id":2,"name":"Austin Satay Limits","start":1443801600000,"end":1444017600000,"address":"456 Skewer Ave","city":"Austin","state":"TX","country":"US"}]}

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.

# ... <snip> ...

# CSRF Configuration
play.filters.csrf.errorHandler=controllers.responses.CSRFFilterError
play.filters.csrf.cookie.name = "CSRF-Token"
play.filters.csrf.header.name = "X-CSRF-Token"

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.

$ curl -i -w '\n' \
-H "X-CSRF-Token: 997917baae485a6382e2092e03e39e5ba848ba3b-1438014702768-7c407d745322c7cd20e87979" \
 --cookie "CSRF-Token=997917baae485a6382e2092e03e39e5ba848ba3b-1438014702768-7c407d745322c7cd20e87979" \
http://localhost:9000/orders/ \
-d '{"ticketBlockID":1,"customerName":"Non Friendly",'\
'"customerEmail":"attaxor@semisafe.com", "ticketQuantity":9876}'

HTTP/1.1 415 Unsupported Media Type
Content-Type: text/html; charset=utf-8
... More error html ...

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.

$ curl -i -w '\n' \
-H "Content-Type:application/json" \
-H "X-CSRF-Token: 997917baae485a6382e2092e03e39e5ba848ba3b-1438014702768-7c407d745322c7cd20e87979" \
 --cookie "CSRF-Token=997917baae485a6382e2092e03e39e5ba848ba3b-1438014702768-7c407d745322c7cd20e87979" \
http://localhost:9000/orders/ \
-d '{"ticketBlockID":1,"customerName":"Non Friendly",'\
'"customerEmail":"attaxor@semisafe.com", "ticketQuantity":9876}'

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 27 Jul 2015 16:45:27 GMT
Content-Length: 154

{"result":"ko","response":null,"error":{"status":1001,"message":"There are not enough tickets remaining to complete this order. Quantity Remaining: 991"}}

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.

$ curl -i -w '\n' \
-H "Content-Type:application/json" \
http://localhost:9000/orders/ \
-d '{"ticketBlockID":1,"customerName":"Non Friendly",'\
'"customerEmail":"attaxor@semisafe.com", "ticketQuantity":9876}'

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Mon, 27 Jul 2015 16:50:27 GMT
Content-Length: 154

{"result":"ko","response":null,"error":{"status":1001,"message":"There are not enough tickets remaining to complete this order. Quantity Remaining: 991"}}

… 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.

# CSRF Configuration
play.filters.csrf.errorHandler=controllers.responses.CSRFFilterError
play.filters.csrf.cookie.name = "CSRF-Token"
play.filters.csrf.header.name = "X-CSRF-Token"
play.filters.csrf.contentType.whiteList = ["*"]
play.filters.csrf.header.bypass = false

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.

EventListEntry = React.createClass

    # ... <snip> ...

    getCookie: (name) ->
        document.cookie.split(';').filter( (c) ->
            c.trim().startsWith("#{ name }=")
        ).map( (c) ->
            c.split('=')[1]
        )

    placeOrder: ->
        # ... <snip> ...

        csrfToken = @getCookie("CSRF-Token")

        ticketBlocksApi.ajax(
            data: JSON.stringify order
            contentType: 'application/json'
            headers:
                "X-CSRF-Token": csrfToken
        )
        .done (result) =>

        # ... <snip>

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.