Preventing cross-site attacks using same-site cookies

Our comms team told us we need an image; our legal team told us it needed to be freely licensed. Credit: Carsten Schertzer (Creative Commons Attribution 2.0)

 

Dropbox employs traditional cross-site attack defenses, but we also employ same-site cookies as a defense in depth on newer browsers. In this post, we describe how we rolled out same-site cookie based defenses on Dropbox, and offer some guidelines on how you can do the same on your website.

Background

Recently, the IETF released a new RFC introducing same-site cookies. Unlike traditional cookies, browsers will not send same-site cookies on cross-site requests. At Dropbox, we recently rolled out same-site cookies to defend against CSRF attacks and cross-site information leakage. We concluded that same-site cookies are a convenient way to reduce a website’s attack surface.

Cross-site requests

Many attacks on the web involve cross-site requests, including the well-known cross-site request forgery (CSRF) attack. These attacks trick the victim’s browser into performing an unintended request to a trusted website. Because users trust Dropbox with their most sensitive data, it’s critical that we make our defenses against these attacks as strong as possible.

What does a CSRF attack look like? As an example, let’s pretend Dropbox was naively not protecting against CSRF attacks. The attack starts when a victim visits an attacker-controlled website, say www.evil.com . The evil website then returns a page with a malicious payload. The browser executes this malicious payload, which makes a request to https://dropbox.com/cmd/delete and attempts to remove user data.

A hypothetical CSRF attack on Dropbox

 

A classic CSRF defense is to introduce a random value token — called the CSRF token — and store its value in a cookie, say csrf_cookie , on the first page load. The browser sends the CSRF token as a request parameter on every “unsafe” request (POSTs). The server then compares the value of csrf_cookie to the request parameter and throws a CSRF error if these values do not match.

Even if a website has CSRF defenses, it could be vulnerable to cross-origin information leakage attacks like cross-site search attacks and JSON hijacking. For example, let’s assume www.dropbox.com has an AJAX route /get_files . A GET request to /get_files gets all of the logged in user’s filenames. and the size of the response can leak side channel information about how many files are there in a user’s Dropbox.

We now describe how we designed and implemented defenses against cross-site attacks on Dropbox using same-site cookies.

Design

For reliability and security, our design for same-site cookie protections should have the following requirements:

  • CSRF defense: the defenses provided should be equivalent to our existing CSRF defense: all POST requests (by default) must be same-site requests, as they are state-changing. GET requests do not require any CSRF protection, but could still be responsible for a different class of cross-site vulnerabilities (see the next requirement).
  • Cross-site information leakage defense: AJAX GET requests should be same-site, as they are a source of many cross-site information leakage vulnerabilities.
  • Availability: Our design should allow for careful rollout and easy rollback. We would still keep our existing CSRF defenses, as same-site cookie protection is still a defense in depth. Further, while rolling out, we should not break existing behavior on valid cross-site GET requests (e.g. a Dropbox link shared via email).
  • Flexibility: Our design should be extensible to allow for cross-site defenses on a variety of request types and routes, not just POST requests and AJAX GET requests.

Cookies become same-site by setting the attribute SameSite to one of two values, strict or lax . When strict , browsers will not send the same-site cookie on any cross-site request. When it’s lax, the browser will only prevent sending the cookie on “unsafe” requests (POSTs) but will allow “safe” requests (GETs).

Let’s say Dropbox stores two cookies: a session_cookie and a csrf_cookie (we’re simplifying a tad).

Further, a POST request to https://dropbox.com/ajax_login on the Dropbox site takes as input the user’s credentials and logs the user in (or equivalently, writes session_cookie ). Dropbox also has many “shared links” on pages with the format https://dropbox.com/sh/…/filename . Users can share these links over email and restrict access to these links.

While brainstorming on how to add same-site cookie protections to Dropbox, we came up with the following naïve designs but quickly figured out that they were flawed:

  • Add the SameSite attribute in strict enforcement mode to session_cookie. This makes all requests after logging in same-site and defends against CSRF and cross-information leakage for all authenticated users. Unfortunately, this can be problematic in cases where users navigate to a Dropbox link shared via email. This would cause cross-site GET requests with an authenticated user to not behave as expected, as the browser will not send that cookie.
Candidate design #1: Dropbox sets the session cookie as SameSite with enforcement mode strict on login

Candidate design #1 isn’t ideal as a benign cross-site GET requests treat the user as logged out even if they were logged in

 

  • Use lax enforcement mode for session_cookie instead of strict . As stated earlier, this would only give us basic CSRF protection, and wouldn’t help us with cross-origin information leakage attacks like cross-site search attacks and JSON hijacking with AJAX GET requests. Further, it’s weaker than our earlier CSRF protection as it doesn’t work for unauthenticated users (e.g. it doesn’t provide a defense against login CSRF attacks).
Candidate design #2: Dropbox sets the session cookie as SameSite with enforcement mode lax on login. CSRF defenses will only work for authenticated requests, e.g. the first login request could have been a cross-site request.

 

  • Have the CSRF cookie csrf_cookie as a SameSite cookie in strict enforcement mode. This, however, makes it harder to incrementally roll out and deal with unexpected failures. Further, this approach doesn’t work because we cryptographically bind the CSRF token to session_cookie to avoid session fixation. When we see the presence of session_cookie , but the absence or invalidity of csrf_cookie , we suspect session fixation and log the user out. Therefore, simple cross-site GET requests will still fail.
Candidate design #3: Dropbox sets the CSRF cookie as SameSite with enforcement mode strict on login.
Candidate design #3 isn’t ideal because we always check whether the session cookie was cryptographically bound to the CSRF token, even on benign GET requests.

 

Instead, we opted to introduce a new cookie, __Host-samesite_cookie . This cookie is SameSite with enforcement mode as strict . We set this cookie on all browsers that support same-site cookies, and we validate this cookie on every relevant request on the same browsers.

The value of this cookie is derived from the CSRF token. We validate the same-site cookie by checking for its presence as well as the correctness of its value. We check for the value to defend against session fixation in case a cookie with the same name got set by an attacker previously.


Final design: Introduce a new cookie with SameSite set to strict and value derived from the CSRF token.

 

__Host-samesite_cookie has an enforcement mode strict , which means it does not get sent on benign cross-site GET requests (e.g. visiting a Dropbox public link from an external page). This is fine, as we can control enforcement on server-side. If the request is a benign GET request but __Host-samesite_cookie is absent, we can still allow the request to pass through. However, if it’s a state-changing POST request and __Host-samesite_cookie is absent, we can treat this as a CSRF error.

We can control enforcement on the server side. Benign GET requests will be allowed, as we can ignore samesite protection on non-AJAX GET requests.

 

As an aside, we made __Host-samesite_cookie a __Host-prefix cookie. A __Host-prefix cookie is a cookie that must only be sent to the host that sent the cookie. JavaScript on a subdomain of www.dropbox.com cannot set this cookie. If both csrf_cookie and __Host-samesite_cookie are valid, we can be confident that no session fixation attacks have occurred.

Rollout

Dealing with cookie authentication is very risky. It could create many availability and security issues. In the worst cases, it could lock many users out (and force them to manually reset their cookies), log users into another user’s account, or completely disable our CSRF defenses!

Therefore, we decided to roll out same-site cookie defenses in two stages: first in “warnings-only” mode, where we log all errors, and later in “enforcement” mode when we see no unexpected errors. Further, we would want to be flexible in terms of what kinds of requests we would like to enforce the same-site check for. Enabling the same-site check for POST requests only would be equivalent to our current CSRF check, but wouldn’t necessarily be a defense against cross-origin information leakage or be helpful with entry-point investigation.

To recap, we added a new cookie __Host-samesite_cookie for browsers that support the cookie. The cookie is SameSite with enforcement mode strict . Its value is derived from csrf_cookie . For the following requests, we check for presence of __Host-samesite_cookie and validate:

  1. If the request method is “POST”, and the route is NOT whitelisted for skipping CSRF protection, report a CSRF error if __Host-samesite_cookie is invalid. (Note that we allow some logging routes to skip CSRF protections.)
  2. If the request is AJAX and the request method is “GET”, report a potential cross-origin information leakage error if __Host-samesite_cookie is invalid.
  3. If the request is non-AJAX and the request method is “GET”, log the route as a potential entry point if __Host-samesite_cookie is invalid.
if not is_present_and_valid(cookies.get("__Host-samesite_cookie")):
  if request.method == "POST" and not skip_csrf_check(route): # Case 1
    report("CSRF error", route)
  elif request.type == "AJAX" and request.method == "GET": # Case 2
    report("Cross-origin error", route)
  elif request.type != "AJAX" and request.method == "GET": # Case 3
    log("entry_points_log", route)
  else:
    pass

In case (1), we noticed minimal false alarms, so we switched from warnings to enforcement mode, raising an HTTP 403 in case of a violation.

For case (2), we noticed that a few AJAX GET requests, such as the ones used by the Saver are cross-site by design. We whitelisted these few endpoints from the same-site check. Further, we noticed on service worker AJAX GET fetches, __Host-samesite_cookie wasn’t sent. We filed a bug report on Chrome. For those service worker routes, instead of relying on the cookie check, we added an additional header to block cross-site requests. After this, we were confident that we could roll this out in enforcement mode.

For case (3), most websites have very few “toplevel” pages like https://www.dropbox.com and https://www.dropbox.com/help , and many pages users navigate to from these base pages, e.g. https://www.dropbox.com/team/admin/members , but do not visit directly. We call these toplevel pages entry points. Enforcing that users can visit non-entry points only by navigating to from entry points can reduce the attack surface of a website.

By leveraging same-site checks for all non-AJAX GET routes, we found a few non-entry points, such as:

  • routes a Dropbox team administrator visits to manage their team members’ accounts.
  • routes in our support flow, where the user answers a series of questions before contacting customer service
  • some legacy routes that relied on clicking on an “are you sure?” yes/no dialog box before performing an action

However, we noticed that our website has much fewer non-entry points than we expected. We suspect that modern web applications might not have many non-entry points, but we’d love to hear your thoughts.

Conclusion

Same-site cookies are a convenient way to defend against a variety of attacks using cross-origin requests. Because it’s not supported on all browsers, it’s best as a defense in depth measure. We hope that other browsers will implement this feature in the future.

Rolling out out same-site cookie defenses on top of existing CSRF defenses should give added security benefits without disrupting availability. For reasons outlined in this post, we recommend adding a new cookie, with SameSite in strict enforcement mode, and controlling the actual same-site enforcement on the server side. This cookie should preferably be a __Host-prefix cookie and its value should preferably be derived from the CSRF token.

Dropbox is leveraging the security benefits of same-site cookies made possible by new browsers. Security is core to our company, and we’re excited to add another layer of protection for our users and their data.