Alex's Notes

CS253 Lecture Summaries: Part IV: CSRF, Same Origin Policy

Cross-Site Request Forgery (CSRF)

There’s a problem with ambient authority - it can be unclear which site initiated a request.

Consider a site attacker.com that includes the following image tag:

<img src='https://bank.example.com/withdraw?from=bob&to=mallory&amount=1000' />

Browser will include all bank.example.com cookies in all requests, even though the request originated from attacker.com, so if we’re logged in and have a valid session id. Note attacking site can’t see the request or the cookies, it’s a shot in the dark.

This is like a reverse xss! It’s called Cross Site Request Forgery An attack which forces a user to execute unwanted actions on a web app in which they are currently authenticated.

Normal users: CSRF attack can force user to perform requests like transferring funds, changing email address etc.

Admin users: CSRF attack can force admins to add new admin user, or run commands on the server.

They’re effective even when the attacker can’t read the HTTP response.

Note you can do this with post requests too. Here’s one way:

Include an invisible iframe with a form that sends the post request on submission to the other site. auto submit it. the frame will redirect, but won’t be visible to the user. If the user is authenticated the server will treat it as a valid post request…

Forms are allowed to be submitted from one site to another by default. Problem!

Here’s one partial solution - can we remove the ambient authority when the request comes from another site?

There is another attribute, the SameSite attribute, which prevents a cookie being sent with requests initiated from another site. We can set it as:

SameSite=None default, always send cookies. Use cases include if you want to authenticate users automatically when your page is loaded in a frame (like FB comments), or if you want to track users across other sites (ad tracking).

SameSite=Lax withold cookies on subresource requests originating from other sites (eg from an image on a page, or a form submitted on a page, or within an iframe), allow them on top-level requests (ie page navigation). Often the best setting for normal sites.

SameSite=Strict only send cookies if the request originates from the website that set the cookie. IE this will block cookies being added even if you’re linking from your site to another. The browser won’t attach the other page’s cookies on that initial request originating from your link. This can actually be a bit confusing to users. Let’s say you click a link from social media to a news site. You’re logged into the news site in another tab, you won’t be when you click that link. But if you’re doing sensitive stuff you may want to be this strict.

So a standard cookie setting for a normal site might look like this:

Set-Cookie: key=value; Secure; HttpOnly; Path=/; SameSite=Lax

There are proposals to make Lax the default, and if None is set then they must be marked as Secure so that tracking is not visible in public. Not sure whether this is in Chrome yet?

Note you have to specify the attributes when you clear the cookie, otherwise the browser will interpret it as a new cookie with the same name.

Same Origin Policy

What do we want to allow on the web?

Should site A be able to link to site B? Surely yes - how the web works.

Should site A be able to embed site B? Seems useful but dubious in some circumstances.

Should site A be able to embed site B and modify its contents? Seems dodgy.

Should site A be able to submit a form to site B? Allowed but has issues.

Should site A be able to embed images from site B? Can be useful for sure, eg CDN use case.

Should site A be able to embed scripts from site B? Seems dodgy

Should site A be able to read data from site B? eek.

The key security model of the web, and the main takeaway is the Same origin policy that two pages from different sources should not be allowed to interfere with each other.

At a high level, we can think of the web as analogous to an operating system. An origin is analogous to an OS process. The browser is analogous to the kernel. Sites rely on the browser to enforce the system’s security rules. The job of the OS is to keep processes isolated, unable to interfere with each other’s memory etc, and the same with the browser and origins.

Just like OSes, if there’s a bug in the browser the rules go out the window.

The basic rule

Given two separate JavaScript execution contexts, one should be able to access the other only if the protocols, hostnames, and port numbers associated with the host documents match exactly.

The protocol-host-port tuple is called an origin.

So the principle is simple, but the devil is in implementation.

Which actions should be subject to security checks?

Where does one document begin or end?

How much interaction should be allowed between non-cooperating origins?

So pages can embed each other. They can’t reach into and touch the document of an iframe on a different origin.

What about using the fetch API? Can I just use JS to fetch the source of another site? Not necessarily - the browser would send the request with your cookies attached, and we don’t want that (imagine a malicious site firing a fetch request to an email site, it would get all your emails), so the CORS policy in the browser blocks the request.

Problems with the same origin policy - sometimes the policy is too narrow. Difficult to get subdomains to exchange data.

Sometimes the policy is too broad - no way to isolate https://web.stanford.edu/class/cs106a/ from https://web.stanford.edu/class/cs253/

Policy is not enforced for all features, you need to know which ones…

Co-operation

document.domain

Sometimes we want to cooperate with another origin. Two cooperating sites can agree that for the purpose of Same Origin Policy checks they want to be considered equivalent. This is for when an origin has a reference to another (like in an iframe or pop-up window).

Sites must share a common top-level domain.

For example ‘login.stanford.edu’ and ‘axess.stanford.edu’ may both make the assignment:

document.domain = 'stanford.edu'

This will set the common domain. You can even do it to just .edu but this is crazy, don’t do it.

This is a really bad idea in general though. If two subdomains want to communicate, they have to set their domain to their ‘parent’, which then allows any child of their parent to communicate with them. So an attacker on the subdomain can attack them easily.

Note that both sites have to explicitly declare the same domain, a subdomain can’t just declare to the more general one and get content on the more general one. So stanford.edu declaring the above domain is not a no-op.

iframe messages

What if we encoded data in URL fragment identifiers? That doesn’t cause a page to reload. If we didn’t match the fragment it doesn’t do anything.

Gap in same origin policy! Parent is allowed to navigate child iframes. Child can poll for changes to the fragment identifier.

Then the fragment becomes a messaging format!

This is how we used to do it before the API was introduced.

Now we have the postMessage API. This allows secure, cross-origin communications between cooperating origins.

Send strings and arbitrarily complicated data cross-origin.

Useful featues:

  • “structured clone” algorithm used for complicated objects. Handles cycles, can’t handle object instances, functions, or DOM nodes.

  • “transferrable objects” allows transferring ownership of an object. It becomes unusable (neutered) in the context it was sent from. Useful if you want to transfer a large object that would take a long time to copy over.

To send a message from the parent to the child you do:

iframe.contentWindow.postMessage(<message>, <iframe origin>)

To listen you do:

window.addEventListener('message', <callback>)

When listening to a message check the origin of the message! Use event.origin to get it.

So here’s an example:

//in the parent iframe
window.addEventListener('message', event => {
  setCurrentUser(event.data.name);
})

//in the child
const data = {name: 'Bob Sullivan'}
window.parent.postMessage(data, '*')

this is insecure - you’re posting the name to any domain that embeds you in the child, and in the parent you’re not checking where the message is coming from…

So you need to validate the destination of messages (you can do that natively by specifying the domain rather than just wildcarding it.

window.parent.postMessage(data, 'https://mytrustedsite.com')

And if you’re receiving messages you need to do something like this:

window.addEventListener('message', event => {
  if(event.origin !== 'https://mytrustedsite.com') return
  //otherwise carry on
  }

Always specify origin in postMessage.

Remember the automatic exceptions to same origin

  • you can embed an image from another site,

  • you can link to a stylesheet from another site,

  • you can post a form to another site,

  • you can load a script from another site.

remember ambient authority is implemented by cookies, so you can embed an image from another domain and the browser will send the cookies.

You could check the referer, but beware of the browser caching the resource, then it will never hit your server, and the referer check will not happen. Just use SameSite cookies.

Cookies and Same Origin Compared

Cookies were created before the same origin policy so have a different security model.

On the one had they are more specific than same origin, since you can scope them to a particular path.

But we know that’s ineffective for security because the same origin policy means that pages on the same origin can affect each other’s DOM.

On the other hand they are less specifi than the same origin policy, since different origins can mess with each others cookies (eg attacker.stanford.edu can set cookies for stanford.edu).

This why we use subdomains for anything sensitive like login.mysite.com NOT mysite.com/login.

Here’s an example of an issue:

Let’s say an ad page is set up so that nonexistent.example.com displays an ad page from the ISP.

It’s on a different origin from example.com but can access its cookies. A malicious script on the ad page could get those cookies.

Also if there’s an XSS issue on the ad page, anyone could steal those cookies by making a user visit nonexistent.example.com/<attack code> The DNS is hijacked by ad page, runs the attack code, and the cookies are stolen.