- Welcome to the last talk of Saturday,
today, here at DEF CON, in track three.
So, this is the CSRF Resurrections,
starring the Unholy Trinity: Service Worker of PWA,
SameSite of HTTP Cookie and Fetch.
Please welcome Dongsung Kim.
(audience applauds)
- Oh.
Right.
Sorry for making you wait. Thank you. (laughs)
(audience applauds)
Well, this is the last time so I can use
whatever the time I have.
Thank you. Good evening.
Thank you for all for coming to my talk.
So great to see you all in person.
So I'm excited to present my research,
The CSRF Resurrections!
Starring the Unholy Trinity: Service Worker of PWA,
SameSite of HTTP Cookie, and Fetch.
That's a long name.
Before we begin, let me introduce myself.
My name is Dongsung Kim, I'm an IT Security Expert
at Security Office.
We are based in Sweden and in South Korea,
and are part of the Truesec Group.
If you have any questions or comments,
please feel free to reach out.
So firstly, let's touch on the current landscape
of the web and what we're going to discuss.
The web platform is evolving faster than ever before.
Every day, new features are, and APIs are constantly
being proposed, developed, tested, shipped,
and standardized.
They not only include aesthetic improvements,
but also open powerful operations to web developers
such as WebUSB, WebGPU,
NFC, Serial, Sockets, Sensor, etcetera.
Then what does it mean for security?
Well, new technologies bring new
security problems to consider.
They are intertwined with each other which by nature,
it is highly complex to maintain.
Previously, researchers like Franken et al.
took a look at broader features, such as PDF JS, AppCache,
prerender, WebSocket, et cetera,
and they discovered that for all browsers,
at least one cookie protection policy can be bypassed.
Some of the findings that had been fixed were reintroduced.
Meltdown and Spectre,
they changed our way of thinking about the traditional
web threat model, powerful web APIs,
such as performance or SharedArrayBuffer
had to be disabled or reduced.
This show the complexity of keeping the web secure,
although every step of the way, it gets thoroughly reviewed,
errors slip through the cracks.
In this talk, I will put my focus on that complexity,
especially we'll discuss HTTP Cookie SameSite,
PWA Service Worker, Fetch,
then how the three can bring back CSRF from the dead.
So let's dive in.
So first, SameSite Cookie,
the conclusive antidote to CSRF.
So, but before we even get to SameSite Cookies,
let's start with what actually is HTTP Cookie.
So HTTP Cookie is a state management mechanism
built on top of the stateless HTTP protocol.
It defines two headers, a Cookie for requests,
Set-Cookie for responses.
Typically used to store server's session identifier,
often a random secret in the client storage.
Then user agents or clients or browsers
always send the appropriate Cookie value,
but only with the matching requests.
What we're gonna focus is the matching part.
What does that matching mean?
If we dissect the below example into parts,
SESSION is the cookie value,
no, Cookie name, sorry.
Secret nonce is the Cookie Value.
Domain and Expires are the Attribute Names,
and example.com and the written date
are the Attribute Values.
So matching is mainly about the attributes.
The Domain attribute states that this Cookie is for
example.com and the Expires attributes
states that it should be discarded after that specific date.
Assume the user agent accepted the Set-Cookie header
when the user agent makes a request like img.gif.
Since the domain is a match with the server
and the expiration data is a match with the clock,
it will include the Cookie Value in the Cookie Header.
That's kind of about it. And that is a problem.
The Cookie standard, put it bluntly, quote:
For historical reasons,
cookies contain a number of security
and privacy infelicities.
Just to name a few, third-party cookie,
super cookie, eavesdropping, cross-site scripting
as cross-site request forgery.
What we're going to focus on is the cross-site
request forgery.
Then first, what is cross-site anyways?
What is cross-site and cross-site request anyways?
Let's consult the specifications.
First, we need to define what an origin is.
An origin is defined as a tuple of three elements.
A scheme, host and port.
Below on the left side are URLs
and the right side are the origins.
A URL like https://example.com/page
becomes a tuple of https://example.com:443
as it's origin.
If the host is just different,
like sub.example.com or if the port is different,
port is custom like 8443, the tuple changes,
hence the origin is different.
Then we need to define what our registrable domain is.
It is a domain name the internet users can,
or historically could (indistinct).
A registrable domain is defined as a public suffix of host
plus one additional label.
A Public Suffix List is a database
of effective top level domains.
It contains names like com, co.uk, github.io,
ngrok.io, et cetera.
So if the host is example.com,
then its public suffix becomes just com,
then plus one additional label becomes example.com.
Which is its registrable domain.
For sub.example.com,
its public suffix is com,
so registrable domain is example.com.
The same as the last one.
The registrable domain of a sub domain is the same
as its parent domain.
Next, we define what a site is.
A site is defined as a tuple of two elements:
scheme, registrable domain.
We are using the same examples again,
and the schemes are the same,
but the host and ports are slightly different.
But since the registrable domains are all example.com,
the site, a tuple of https, example.com,
are all the same.
Hence the origins are all same-site with each other.
Then we need to choose which one should be the site for
cookies for a given request.
This varies under different circumstances,
but for the usual browsing cases,
it is the documents top-level origin,
also known as the URI displayed in the address bar.
Simple as that.
We need to define one more thing
and the client object of a request.
This is an object that holds among other things,
context for JavaScript and browsing
and an origin used in security checks.
We'll denote it with curly brackets like so.
With this information, we can finally define
what a SameSite request is.
A request is SameSite when it satisfies A,
the request's URL's origin is SameSite
with its client's site for cookies,
B, the request URL's origin is SameSite
with every URL in its URL list,
C, the request is not triggered by a user
pressing the reload button.
Moving forward, we will only focus on the first criteria.
And then a cross-site request is defined
to be a request that is not SameSite.
That was quite formal,
but so let me give you a basic example
to make it more digestible.
As we discussed,
a site is a tuple of scheme and registrable domain.
And the typical site for cookies
is the user agent's address bar.
Below is a document hosted at https, example.com.
And this is our site for cookies
for the rest of the example.
Its site is a tuple of https, example.com.
On this page,
there's a link to https, example.com
whose site is a tuple of https, example.com.
So clicking the link will spawn a request.
Since the request URL's origin's site is the same as
that of site for cookies, the address bar,
it is a SameSite request.
Also, clicking a link will make the browser
navigate to the URL.
It's a navigation request and also top-level
since the document does not have a parent document.
It is also an HTTP safe request
since as per the HTTP semantics specification,
the GET method is considered safe.
Embedding an image will also spawn a request.
The URL is https://example.com/image.gif.
Like the previous link, it is a SameSite request.
If a request is for an image, audio, script style,
et cetera, we call it a subresource request.
It is an HTTP GET, safe request, just like clicking a link.
Now there is a different link to https, victim.com.
Its site becomes a tuple of HTTPS and victim.com,
which is different from that of site for cookies,
the address bar.
It is a cross-site request.
Just like the previous link, it is a top-level
navigation request and then HTTP safe request.
Finally, we have a form for https, victim.com.
Like the previous link,
it is a cross-site request.
Submitting the form will make the browser
navigate to the URL.
So it is a top-level navigation request.
But since the form's method attribute is POST,
it is now an HTTP unsafe request.
Now we know what a cross-site request is,
we can define what a CSRF is.
It is when an attacker forces our victim to execute forged
cross-site requests.
It's possible because as we discussed,
user agents send the cookies with matching requests.
Below is an example.
Let's say user is visiting example.com
and it has a forged form which has malicious inputs
like delete and all.
Submitting the form to victim.com by making the user click
on it or using JavaScript, will spawn a cross-site request,
also, a top-level navigation request,
and an HTTP unsafe request.
Crucially, the user's cookie will be sent.
The server will recognize and authorize it as the user
to run the malicious command.
It is indeed possible to perform a CSRF with the HTTP GET
method like clicking a link or embedding an image,
as long as the server is susceptible.
There are many known (indistinct) to mitigate CSRF.
These are the mitigations.
And one of them is to adopt the SameSite policy.
So the SameSite attribute introduces an additional
attribute to the matching process.
If the attribute is set to strict, then the browser
send the cookie only for SameSite requests.
The browser does not send the cookie for clicking
a cross-site link, nor embedding a cross-site image,
nor submitting a cross-site form.
But SameSite=Lax is like strict,
but there's an exception.
It allows cross-site top-level navigation safe requests.
What it means is that
the browser will send a cookie
only when clicking a cross-site link,
with our previous examples,
but not when embedding a cross-site image,
but not when submitting a cross-site form.
If it is set to none,
the browser will behave just like before.
The SameSiteness is ignored and the cookie is sent.
Let me show you how it's all supposed to work.
Right, we start with the victim page here
where it echoes the cookie header of the browser's request.
It shows three cookies,
none-cookie=1, lax-cookie=2, strict-cookie=3.
And you can see the SameSite attributes
are set respectively.
Let's navigate to the attacker page
and then navigate back to the victim page
by simply clicking a link using GET method.
And now the page echoes only two cookies, none and lax.
Meaning the browser did not send a strict cookie,
since it was a cross-site request.
Even though they are actually properly stored.
We tried same by submitting a form using POST request.
Now the page shows only one cookie, none,
meaning the browser did not send lax-cookie either
since the request was HTTP unsafe.
So that is a quick look at how it's all supposed to work
with SameSite attribute.
And the SameSite attribute has been widely supported
by browsers, but the adoption has been kind of slow.
Then in 2020, Chrome started treating cookies
without SameSite attribute as Lax by default.
The cookie draft standard has been also revised.
So there has been some sentiment that CSRF dead,
is really dead and SameSite cookies alone
are sufficient to protect against CSRF.
We're gonna leave it at that
and move on to a different topic.
And the next is...
Our next topic is Service Worker, PWAs Network Layer.
So service worker is one of the core technologies
of progressive Web App.
Progressive Web Apps in a nutshell
is a type of web application that is native-like,
installable and offline.
Service worker runs in a worker context,
meaning it is running in a different thread
from the main JavaScript execution context.
Service worker can act as a proxy server
between web application, browser and network.
It can intercept network requests,
then respond with custom responses.
Together with CacheStorage API,
it makes offline fallback pages possible
in a web application.
Service worker is mainly used for custom caching strategies
by a front-end web developer,
just like Twitter on the right example.
So this is how caching works with service worker.
There are roughly four layers to consider,
the HTML, CSS, JavaScript layer on the top,
in the middle,
the service worker interacting with CacheStorage API,
the browser with its HTTP Cache, and the server.
So let's say the HTML layer requests an asset.
It could be an image, a video, a CSS file,
a JS file, et cetera.
Then a FetchEvent is dispatched to the service worker,
which intercepts the request.
Here, a web developer can implement their custom caching
strategy, using JavaScript and CacheStorage API.
In case of a cache miss, or an expiration, or else,
the service worker can call the Fetch API
using the events request.
Then the browser makes use of its HTTP cache strategy
based on the headers, Cache-Control, Expires, etcetera.
If that also encounters a cache miss or else...
(clears throat)
Excuse me.
The browser makes an HTTP request to the server
over the network and receives an HTTP response.
The HTTP response is handled, then passed to
the service worker in the form of a response class object.
The service worker then receives the object,
stores it in CacheStorage, then passes into the event.
So let's implement this flow with a quote,
Example from the perspective of a web developer.
Before anything,
we register the service worker.
This does not require any user interaction
and the developer will typically just want it to be happen
as soon as possible.
The service worker looks like this,
it adds an EventListener on Fetch
then starts intercepting all requests.
If there is a request, in this example, it's an image,
it triggers and dispatches a Fetch event to the listener.
Now the callback function is called to handle the request.
The function checks if there is an existing match
in the CacheStorage, if there is a match,
it is returned to the event.respondWith method.
We call this the Cached Flow.
If there is no match,
we call the Fetch API simply using the events request as is.
We call this the Passthrough flow.
The browser returns a response object,
which also gets returned untouched.
Then finally, the image receives the response object.
The Cached flow isn't required,
the service worker can simply pass through every request.
Then we will revisit this example later.
For more complex or caching strategies,
most web developers rely on an Open Source Library
to make the job easier.
One of the most popular ones is Workbox
created and maintained by Google Chrome.
It is a collection of JavaScript libraries for PWAs.
Importantly,
it includes service worker routing and caching modules.
We simply import the workbox-sw.js,
then the developer can specify
whatever custom cache strategy they want.
In this Workbox example number one,
a route is registered in such that if the request
is for an image, it uses the strategy
that looks up the cache first,
effectively caching all the image assets.
Then a default handler is registered so that all the other
unspecified requests will only go to the network.
Something like this is also possible,
Workbox example number two,
a route is registered such that if the request URL
has a path name of /transfer,
it should only go to the network,
but will time out after 600 seconds for POST requests.
The intention is to make the request wait
a little bit more because the server is slow somehow.
For React development, Create React App
provides a built-in support for service worker
and caching.
Inside index.js,
there is a line that can be changed.
Changing the method to register, registers service/worker.js
as soon as the page is loaded.
It is also using Workbox under the hood and customizable.
We'll also revisit the examples later.
Now that we learned SameSite cookie and service worker,
let's get into the main part,
the collaboration that leads to CSRF.
We will take a deep dive into the specifications
to spot the smells radiating from them.
This is going to be a bit crude and simplified read,
so bear that in mind.
The scene is familiar,
it is the same example.
We have a service worker registered,
intercepting and passing the request through.
Then we have an image element waiting to be loaded
by the browser.
As per the HTTP specification,
when updating the image data,
the browser should simply fetch the image request.
But how does that actually happen?
As per the fetch specification,
the browser should perform fetching, main fetch,
scheme fetch, and HTTP fetch.
In the middle of the process,
if we have an active service worker, 5.4,
set response to the result of invoking handle fetch
for requestForServiceWorker.
Then what is handle fetch?
We look at the service worker's specification.
Appendix A: Handle Fetch,
let e be the result of creating an event with FetchEvent.
Dispatch e at activeWorker's global object.
Now, the event is created and is passed on
to the service worker.
The service worker code is the same.
It is a passthrough.
Simply fetch the events request,
then responds with return the response as is.
But then what does that fetch method do?
As per the fetch spec,
the fetch input, init method steps are,
let request object be the result of invoking
the initial value of request as constructor
with input and init as arguments.
That is one sentence.
Then let's see what the constructor does.
For the request class,
6.2, set request to input's request,
then 12, set request to a new request
with the following properties.
What's happening here is that the browser creates
a new request object out of the input request object,
copying one property at a time.
But then there's something peculiar,
the origin of the new request is set to client,
a placeholder string,
meaning it will follow it's client object.
Then what's the client object?
It's this object's
context and origin.
This is context and origin.
At this point,
"This" points to the service worker,
rather than the requested image or the HTML.
This is a red flag.
Then how is the cookie header computed?
Let's continue fetching.
Let cookies be the result of running
the "cookie string" algorithm.
See section 5.4 of COOKIES.
Cookies refers to the IETF spec.
So let's look into that.
And in that spec,
there is a separate section for service workers.
It reads, quote:
Requests which are initiated by a service worker itself,
via a direct call to fetch, for instance, on the other hand,
will have a client which is a ServiceWorkerGlobalScope.
Its "site for cookies" will be the registrable domain
of the service worker's URI, unquote.
So some familiar words here,
but the point is that "site for cookies"
is always the service worker's URI,
rather than that of the image request,
same situation, same red flag.
What happens if attacker.com comes into play,
which embeds a cross-site request to victim.com?
The service worker is hosted at victim.com.
"Site for cookies" is always the service worker's URI.
The request URL, victim.com/image.gif is same-site
with its site for cookies, victim.com/sw.js.
Suddenly, it is a SameSite request by definition.
Looks like CSRF is back. So let's put it to the test.
We are going to revisit the previous code examples,
but this time we are adding an attacker.
The attacker has two elements, a link and a form.
Activating other will trigger a FetchEvent,
then passthrough to the server.
Let me show you.
Okay.
Right.
We have the same setup, a victim page,
which echoes the cookie header, none, lax, strict-cookie
set in the browser.
Let's register the service worker.
Now, it is listening for fetch events and the code itself
is simple passthrough.
Let's navigate to the attacker page,
then navigate back to the victim page with a link.
As per the SameSite policy,
the strict-cookie should not be sent.
It is a cross-site request.
Only none and lax should be sent.
But now it shows all three cookies.
The browser simply disregarded the SameSite policy.
Let's try the same with the form.
Now as for the SameSite policy,
now only the none-cookie should be sent.
But all three cookies again.
(audience applauds)
- [Dongsung Kim] Thank you.
Now let's try that again without the service worker.
Then it's resent and it's none-cookie alone.
So...
Then let's look at the Workbox example number one, again,
the developer's intention was simply
to cache all image assets,
and then for all the other requests,
to go over the network only.
And then we will add an attacker to the mix.
The attacker has two elements,
a link and a form to the endpoint/transfer.
We have the victim page here
with login and log out functionality.
Clicking the login button would set a session cookie
with SameSite strict.
You can see the page shows we are authorized.
Let's navigate to the attacker page
and navigate back to the victim with link.
As per the SameSite policy, the strict cookie is not sent,
so we are unauthorized.
Trying again with the form.
Still unauthorized.
So let's go back and let's register the service worker.
It's listening for fetch events and the code looks,
as we discussed.
Let's try...
Let's try the same and click on the cross-site request.
And we are now authorized.
However, submitting the form
does not get us authorized.
So what happens is that...
Go.
What happens is that setting the DefaultHandler itself
makes the service worker passthrough all the requests.
Doing so only applies to HTTP GET requests,
hence only affecting the link, not the form.
However, Workbox example number two can make things worse.
The intention was to simply change the timeout setting.
Let's add the same attacker to the mix.
We have the same victim here,
but there is an additional logic in the service worker,
a prolonged network timeout for POST request transfer.
Let's try the form on the attacker page,
and now we are authorized.
(audience applauds)
- [Dongsung Kim] Thank you.
Let's revisit the Create React App example.
The attacker has one element,
a link to the victim's secret image.
We navigate to the victim like so.
Immediately, there is a service worker
registered to the browser,
listening for fetch events.
Clicking the login button
would set a strict session cookie.
You can see, we are logged in.
Let's navigate to the attacker page
and navigate back to the victim page.
You can see the image here despite the SameSite policy.
Just to be sure, let's deregister the service worker
and try again.
Now it returns forbidden as intended.
And there is one more thing here,
we are using this first example again.
We are doing the same demo one more time,
but this time with the Firefox Developer Tools
opened on the network tab on the right.
(keyboard clacking)
So register a service worker,
listening for fetch events,
navigate to the attacker.
Then the client wrongly sends all three cookies, right?
So it's the same.
But when we examine that specific request
in the Developer Tools,
and the request, it says only none-cookie was sent.
This was what's supposed to happen,
but what actually happened is quite opposite of this.
(keyboard clacking)
Okay, let's talk about patches and updates,
yet issues still remain.
So I reported this problem in the August of 2020.
As it impacted both Firefox and Chrome,
two separate reports were sent.
And then in October,
an issue on the Cookie specification
was independently opened.
It was about the "site for cookies" problem
that we previously discussed.
Then in the August of 2021,
a security bug on the origin header
was independently reported.
It was related to the origin problem
we previously discussed.
In September,
other researchers started noticing the same problem.
So duplicate bugs were independently reported.
In October,
a service worker spec meeting on the CSRF issue was held.
It spawned about 14 separate issues in different specs,
including service worker fetch and storage partitioning.
Three weeks later, Chromium patched both bugs.
And in December, the fetch spec was amended.
As we discussed,
the problem was that the origin of the new request
was client, and the client was pointing to this,
which was the service worker itself.
Now, the amended spec properly propagates
the original request's origin information.
In the January of 2022,
finally Chrome 97 was released
to the stable channel with the fix.
And the next day, Google assigned a CVE.
On July 27th,
the Cookie spec was amended
so that the site for cookies logic
is handled by the service worker spec.
So no update for Firefox is available as of now.
The affected versions are Google Chrome
from 51 up to 97, and Mozilla Firefox from 60,
and for the Developer Tools bug is from 74.
There still is one remaining issue,
and that is a nested frame.
In case of victim one,
let's say news.com, and embeds an attacker ad.com,
which also embeds victim two, news.com.
Since the nested frame and its parent,
the nested attacker frame, are cross-site,
you cannot set nor send a strict cookie.
But if we register the passthrough service worker,
listening to fetch events, now access is possible.
(audience applauds)
This particular problem is to be addressed
with the Client-Side Storage Partitioning spec.
So then conclusions.
To recap,
in this talk, we discussed SameSite of HTTP cookie
centered around cross-site request and CSRF.
We discussed SameSite attribute and Lax by default
as a defense against CSRF.
Next, we discussed service worker of PWA.
We discussed the caching flow with service worker
and code examples.
Then we moved on to the CSRF Resurrections.
We took a deep dive into the related web specs
and how SameSite defense gets bypassed
when a service worker is involved.
Using this code examples,
I showed how a developer can easily introduce this problem.
Finally, we talked about the aftermath.
The disclosure timeline was mentioned,
and we discussed the remaining issue of a nested frame.
Let's finish up with takeaways.
Firstly, do not use SameSite cookies
as the sole defense mechanism against CSRF.
They should be considered as an additional layer of defense.
As plainly written in the Cookie specification, quote:
Developers are strongly encouraged to deploy the usual
server-side defenses to mitigate
the risk more fully, unquote.
Also, double-check network interactions
with other tools when they're possible.
The browsers' Developer Tools might not actually depict
the reality in some cases.
Since it is still an ongoing problem with Firefox
and the nested frame,
this is a new attack vector until
the issues are fully resolved.
Finally,
is there any other subtle complex specification level
problem that has been overlooked and how do we find it?
And that's it.
I'd like to thank Oscar and Louis for their feedback
and Betty for proofreading.
Thank you all very much for listening.
(audience applauds)