 Welcome to the last talk of Saturday today here at DAFKON and Track 3. So this is the CSRF Resurrection, starting the unholy trinity, service worker of PWA, same set of HTTP cookies and fetch. Please welcome Dong Sun Kim. Right. Sorry for making you wait. Thank you. Well, this is the last time, so I can use whatever the time I have. Thank you. Good evening. Thank you for coming to my talk. It's so great to see you all in person. So I'm excited to present my research, the CSRF Resurrection, starting the unholy trinity, service worker of PWA, same set of HTTP cookies and fetch. That's a long name. Before we begin, let me introduce myself. My name is Dong Sun Kim. I'm an IT security expert at the security office. We are based in Sweden and in South Korea and a part of the TRUFSA 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 are going to discuss. The web platform is evolving faster than ever before. Every day, new features 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 like such as web USB, web GPU, NFC, serial, socket, sensor, et cetera. 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. Basically researchers like Frank and Adal took a look at browser features such as PDFJS, app cache, pre-render, web socket, 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. Millsdown and Spectre, they changed our way of thinking about the traditional web threat model. Powerful web APIs such as performance or shared array buffer had to be disabled or reduced. This showed the complexity of keeping the web secure, although every step of the way gets thoroughly reviewed, errors slip through the cracks. In this talk, I will put my focus on that complexity, especially we will discuss HTTP cookie same site, PWA service worker, then how the three can bring back CSRF from the dead. So let's dive in. First, same site cookie, the conclusive entity to CSRF. But before we even get to same site 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, cookie for request, set cookie for responses, typically used to store server session identifier, or from a random secret, in the client storage. Then user agents or clients or browsers always send appropriate cookie value, but only with the matching requests. What we are going to 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 names, sorry. Session, a 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 attribute 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 image.gif. Since the domain is a match with the server and the expiration date 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 in felicitates. Just to name a few, third party cookies, super cookie, eavesdropping, cross-site scripting, and 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 hpsexample.com slash page becomes a tuple of hpsexample.com443 as its origin. If the host is 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 a registrable domain is. It is a domain name the internet users can or historically could directly register. 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, ngrock.io, et cetera. If the host is example.com, then its public suffix becomes just COM, and 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 hosts and ports are slightly different. But since their registrable domains are all example.com, the site, a tuple of HBS, 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 document's 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, contacts 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 same site request is. The request is same site when it satisfies, A, the request URL's origin, is same site with its client site for cookies, B, the request URL's origin, is same site 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. Then, a cross-site request is defined to be a request that is not same site. 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 registerable domain, and the typical site for cookies is the user agent's address bar. Below is a document hosted at hpsexample.com, and this is our site for cookies for the rest of the example. Its site is a tuple of hpsexample.com. On this page, there's a link to hpsexample.com, whose site is a tuple of hpsexample.com. So clicking the link will spawn a request, since the request URL's origin site is the same as that of site for cookies, which is the address bar. It is a same-site request. Also, clicking a link will make the browser navigate to the URL. It's a navigation request and also top-level request, 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 hpsexample.com slash image.gif. Like the previous link, it is a same-site request. If a request is for an image, audio, script, style, etc., we call it a sub-resource request. It is an hbget safe request, just like clicking a link. Now, there is a different link to hpsexample.com. Its site becomes a tuple of hps 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 an HTTP safe request. Finally, we have a form for hps 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 a victim to execute forged cross-site requests. It is possible because, as we discussed, the 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 deletes and all. Submitting the form to victim.com by making the user click on it or using JavaScript will spawn a cross-site request, so 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 commands. It is indeed possible to perform 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 methods to mitigate CSRF. These are the mitigations, and one of them is to adopt the same-site policy. So the same-site attribute introduces an additional attribute to the matching process. If the attribute is set to strict, then the browser sends the cookie only for same-site requests. The browser does not send the cookie for clicking a cross-site link or embedding a cross-site image nor submitting a cross-site form. But same-site lacks is like strict, but there is an exception. It allows cross-site top-level navigation safe requests. What it means is that the browser will send the 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 same-site-ness is ignored and the cookie is sent. Let me show you how it's all supposed to work. We start with the victim page here where it echoes the cookie header of the browser's request. It shows three cookies, none cookie one, lacks cookie two, strict cookie three. You can see the same-site 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. Now the page echoes only two cookies, none and lacks, 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 the same by submitting a form using post request. Now the page shows only one cookie, none, meaning the browser did not send less cookie either since the request was HP Unsafe. So that is a quick look at how it's all supposed to work with same-site attribute. The same-site attribute has been widely supported by browsers, but the adoption has been kind of slow. Then in 2020, Chrome started treating cookies without same-site attribute as lacks by default. The cookie draft standard has been also revised, so there has been some sentiment that CSRF dead is really dead. And same-site cookies alone are sufficient to protect against CSRF. We're going to leave it at that and move on to a different topic. And the next is our next topic is Service Worker, PWA's network layer. So Service Worker is one of the core technologies of Progressive Web App. Progressive Web App 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 cache storage API, it makes offline fallback pages possible in a web application. Service Worker is mainly used for custom caching strategies by a front-to-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 cache storage 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 fetch event is dispatched to the Service Worker which intercepts the request. Here, a web developer can implement their custom caching strategy using JavaScript and cache storage API. In case of a cache miss or an expiration or else, the Service Worker can call the fetch API using the event's request. Then the browser makes use of its HTTP cache strategy based on the headers. Cache control, expires, et cetera. If that also encounters a cache miss or else, 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 cache storage, then passes it into the event. So let's implement this flow with a code 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 happen as soon as possible. The Service Worker looks like this. It adds an event listener 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 cache storage. If there is a match, it is returned to the event.respondwith method. We call this the cache flow. If there is no match, we call the fetch API simply using the event's request as is. We call this the path through 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 path 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 cached strategy they want. In this Workbox example number one, a route is registered such that if the request is for an image, it uses a 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 slash 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 same-site 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's 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 HP fetch. In the middle of the process, since we have an active service worker, 5.4, set response to the result of invoking handle fetch for request for service worker. Then what is handle fetch? We look at the service worker specification. Appendix A, handle fetch, let E be the result of creating an event with fetch event, dispatch E at active worker'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 path through. Simply fetch the event's request, then 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 init as arguments. That is one sentence. Then let's see what the constructor does. For the request class, 6.2, set request to inputs 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 its client object. Then what's the client object? It's this object, 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, section 5.4 of cookies. Cookies refers to the IETF spec, so let's look into that. In that spec, there is a separate section for service workers. Request which are initiated by the service worker itself via direct call to fetch, for instance. On the other hand, we'll have a client, which is a service worker global scope. Its site for cookies will be the registrable domain of the service worker's URI, unquote. 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 slash image.gif, is same site with its site for cookies, victim.com slash sw.js. Suddenly it is the same site request by definition. Looks like CSR 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 fetch event, then path through to the server. We have the same setup, a victim page which echoes the cookie header. Non-lagged strict cookie set in the browser. Let's register the browser, register the service worker. Now it is listening for fetch events, and the code itself is simple path through. Let's navigate to the attacker page. Then navigate back to the victim page with a link. As per the same site policy, the strict cookie should not be sent. It is a cross site request. Only none and legs should be sent. But now it shows all three cookies. The browser simply disregarded the same site policy. Let's try the same with the form. Now as per the same site policy, now only the non-cookies should be sent. But all three cookies again. Thank you. Now let's try that again without the service worker. Then it's resend and it's non-cookie alone. Then let's look at the work box 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. Then we will add an attacker to the mix. The attacker has two elements, a link and a form to the endpoint slash transfer. We have the victim page here with login and logout functionality. Clicking the login button would set a session cookie with same site 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 same site 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 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... Here we go. What happens is that setting the default handler itself makes the service worker path through all the requests. Doing so only applies to HP get request, hence only affecting the link, not the form. However, workbook 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. Let's revisit the create-react app example. The attacker has one element linked to the victim's secret image. We navigate to the victim like so. Immediately there is a service worker registered to the browser without 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 same site policy. Just to be sure, let's de-register the service worker and try again. Now it returns forbidden as intended. 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 Firefox developer tools opened on the network tab on the right. Register a service worker listening for fetch events. Navigate to the attacker. Then the client wrongly sends all three cookies. So it's the same. But when we examined that specific request in the developer tools and the request, it says only none cookie was sent. This was supposed to happen, but what actually happened is quite opposite of this. 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, 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 the 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 embed an attacker at .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 path through a service worker, listening to fetch events, now access is possible. This particular problem is to be addressed with the client-side storage partitioning spec. So then conclusions. To recap, in this talk we discussed same-site of HTTP cookie centered around cross-site request and CSRF. We discussed the same-site attribute and lexed by default as a defense against CSRF. Next, we discussed service worker or PWA. We discussed the caching flow with service worker and code examples. Then we moved on to CSRF resurrections. We took a deep dive into the related web specs and how same-site defense gets bypassed when a service worker is involved. Using the 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 same-site 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 whenever possible. The browser's 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 factor 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 the feedback and Betty for proofreading. Thank you all very much for listening.