<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: samesite</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/samesite.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-08-26T20:26:31+00:00</updated><author><name>Simon Willison</name></author><entry><title>Quoting Frederik Braun</title><link href="https://simonwillison.net/2024/Aug/26/frederik-braun/#atom-tag" rel="alternate"/><published>2024-08-26T20:26:31+00:00</published><updated>2024-08-26T20:26:31+00:00</updated><id>https://simonwillison.net/2024/Aug/26/frederik-braun/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://lobste.rs/s/98rp8f/cors_is_stupid#c_9dtjao"&gt;&lt;p&gt;In 2021 we [the Mozilla engineering team] found “samesite=lax by default” isn’t shippable without what you call the &lt;a href="https://simonwillison.net/2021/Aug/3/samesite/#chrome-2-minute-twist"&gt;“two minute twist”&lt;/a&gt; - you risk breaking a lot of websites. If you have that kind of two-minute exception, a lot of exploits that were supposed to be prevented remain possible.&lt;/p&gt;
&lt;p&gt;When we tried rolling it out, we had to deal with a lot of broken websites: Debugging cookie behavior in website backends is nontrivial from a browser.&lt;/p&gt;
&lt;p&gt;Firefox also had a prototype of what I believe is a better protection (including additional privacy benefits) already underway (called &lt;a href="https://blog.mozilla.org/en/mozilla/firefox-rolls-out-total-cookie-protection-by-default-to-all-users-worldwide/"&gt;total cookie protection&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Given all of this, we paused samesite lax by default development in favor of this.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://lobste.rs/s/98rp8f/cors_is_stupid#c_9dtjao"&gt;Frederik Braun&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/privacy"&gt;privacy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/samesite"&gt;samesite&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="cookies"/><category term="firefox"/><category term="mozilla"/><category term="privacy"/><category term="security"/><category term="cors"/><category term="samesite"/></entry><entry><title>Exploring the SameSite cookie attribute for preventing CSRF</title><link href="https://simonwillison.net/2021/Aug/3/samesite/#atom-tag" rel="alternate"/><published>2021-08-03T21:09:02+00:00</published><updated>2021-08-03T21:09:02+00:00</updated><id>https://simonwillison.net/2021/Aug/3/samesite/#atom-tag</id><summary type="html">
    &lt;p&gt;In reading Yan Zhu's excellent write-up of the &lt;a href="https://blog.azuki.vip/csrf/"&gt;JSON CSRF vulnerability&lt;/a&gt; she found in OkCupid one thing puzzled me: I was under the impression that browsers these days default to treating cookies as &lt;code&gt;SameSite=Lax&lt;/code&gt;, so I would expect attacks like the one Yan described not to work in modern browsers.&lt;/p&gt;
&lt;p&gt;This lead me down a rabbit hole of exploring how SameSite actually works, including building &lt;a href="https://samesite-lax-demo.vercel.app/"&gt;an interactive SameSite cookie exploration tool&lt;/a&gt; along the way. Here's what I learned.&lt;/p&gt;
&lt;h4 id="background-csrf"&gt;Background: Cross-Site Request Forgery&lt;/h4&gt;
&lt;p&gt;I've been tracking CSRF (Cross-Site Request Forgery) &lt;a href="https://simonwillison.net/tags/csrf/?page=2"&gt;on this blog&lt;/a&gt; since 2005(!)&lt;/p&gt;
&lt;p&gt;A quick review: let's say you have a page in your application that allows a user to delete their account, at &lt;code&gt;https://www.example.com/delete-my-account&lt;/code&gt;. The user has to be signed in with a cookie in order to activate that feature.&lt;/p&gt;
&lt;p&gt;If you created that page to respond to &lt;code&gt;GET&lt;/code&gt; requests, I as an evil person could create a page at &lt;code&gt;https://www.evil.com/force-you-to-delete-your-account&lt;/code&gt; that does this:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;img&lt;/span&gt; &lt;span class="pl-c1"&gt;src&lt;/span&gt;="&lt;span class="pl-s"&gt;https://www.example.com/delete-my-account&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If I can get you to visit my page, I can force you to delete your account!&lt;/p&gt;
&lt;p&gt;But you're smarter than that, and you know that GET requests should be idempotent. You implement your endpoint to require a POST request instead.&lt;/p&gt;
&lt;p&gt;Turns out I can still force-delete accounts, if I can trick a user into visiting a page with the following evil HTML on it:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;form&lt;/span&gt; &lt;span class="pl-c1"&gt;action&lt;/span&gt;="&lt;span class="pl-s"&gt;https://www.example.com/delete-my-account&lt;/span&gt;" &lt;span class="pl-c1"&gt;method&lt;/span&gt;="&lt;span class="pl-s"&gt;POST&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;input&lt;/span&gt; &lt;span class="pl-c1"&gt;type&lt;/span&gt;="&lt;span class="pl-s"&gt;submit&lt;/span&gt;" &lt;span class="pl-c1"&gt;value&lt;/span&gt;="&lt;span class="pl-s"&gt;Delete my account&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;form&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;forms&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;submit&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The form submits with JavaScript the instant they load the page!&lt;/p&gt;
&lt;p&gt;CSRF is an extremely common and nasty vulnerability - especially since it's a hole by default: if you don't know what CSRF is, you likely have it in your application.&lt;/p&gt;
&lt;p&gt;Traditionally the solution has been to use CSRF tokens - hidden form fields which "prove" that the user came from a form on your own site, and not a form hosted somewhere else. OWASP call this the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie"&gt;Double Submit Cookie&lt;/a&gt; pattern.&lt;/p&gt;
&lt;p&gt;Web frameworks like Django implement &lt;a href="https://docs.djangoproject.com/en/3.2/ref/csrf/"&gt;CSRF protection&lt;/a&gt; for you. I built &lt;a href="https://github.com/simonw/asgi-csrf"&gt;asgi-csrf&lt;/a&gt; to help add CSRF token protection to ASGI applications.&lt;/p&gt;
&lt;h4 id="samesite-cookie-attribute"&gt;Enter the SameSite cookie attribute&lt;/h4&gt;
&lt;p&gt;Clearly it would be better if we didn't have to worry about CSRF at all.&lt;/p&gt;
&lt;p&gt;As far as I can tell, work on specifying the &lt;code&gt;SameSite&lt;/code&gt; cookie attribute started &lt;a href="https://github.com/httpwg/http-extensions/commit/aa0722c12ccb367b8f4498e982616064d105a006#diff-70cc0c0600a934d002ea91a4a36d5eb0b7d5edebcce5a40c9a811391cc0fecf6"&gt;in June 2016&lt;/a&gt;. The idea was to add an additional attribute to cookies that specifies the policy for if they should be included in requests made to a domain from pages hosted on another domain.&lt;/p&gt;
&lt;p&gt;Today, all modern browsers support SameSite. MDN &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"&gt;has SameSite documentation&lt;/a&gt;, but a summary is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SameSite=None&lt;/code&gt; - the cookie is sent in "all contexts" - more-or-less how things used to work before SameSite was invented. &lt;strong&gt;Update:&lt;/strong&gt; One major edge-case here is that Safari apparently ignores &lt;code&gt;None&lt;/code&gt; if the "Prevent cross-site tracking" privacy preference is turned on - and since that is on by default, this means that &lt;code&gt;SameSite=None&lt;/code&gt; is effectively useless if you care about Safari or Mobile Safari users.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SameSite=Strict&lt;/code&gt; - the cookie is only sent for requests that originate on the same domain. Even arriving on the site from an off-site link will not see the cookie, unless you subsequently refresh the page or navigate within the site.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SameSite=Lax&lt;/code&gt; - cookie is sent if you navigate to the site through following a link from another domain but &lt;em&gt;not&lt;/em&gt; if you submit a form. This is generally what you want to protect against CSRF attacks!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The attribute is specified by the server in a &lt;code&gt;set-cookie&lt;/code&gt; header that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set-cookie: lax-demo=3473; Path=/; SameSite=lax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why not habitually use &lt;code&gt;SameSite=Strict&lt;/code&gt;? Because then if someone follows a link to your site their first request will be treated as if they are not signed in at all. That's bad!&lt;/p&gt;
&lt;p&gt;So explicitly setting a cookie with &lt;code&gt;SameSite=Lax&lt;/code&gt; should be enough to protect your application from CSRF vulnerabilities... provided your users have a browser that supports it.&lt;/p&gt;
&lt;p&gt;(Can I Use reports &lt;a href="https://caniuse.com/same-site-cookie-attribute"&gt;93.95% global support&lt;/a&gt; for the attribute - not quite high enough for me to stop habitually using CSRF tokens, but we're getting there.)&lt;/p&gt;
&lt;h4 id="samesite-missing"&gt;What if the SameSite attribute is missing?&lt;/h4&gt;
&lt;p&gt;Here's where things get interesting. If a cookie is set without a SameSite attribute at all, how should the browser treat it?&lt;/p&gt;
&lt;p&gt;Over the past year, all of the major browsers have been changing their default behaviour. The goal is for a missing SameSite attribute to be treated as if it was &lt;code&gt;SameSite=Lax&lt;/code&gt; - providing CSRF protection by default.&lt;/p&gt;
&lt;p&gt;I have found it infuriatingly difficult to track down if and when this change has been made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chrome/Chromium offer &lt;a href="https://www.chromium.org/updates/same-site"&gt;the best documentation&lt;/a&gt; - they claim to have ramped up the new default to 100% of users in August 2020. WebViews in Android still have the old default behaviour, which is scheduled to be fixed in Android 12 (&lt;a href="https://en.wikipedia.org/wiki/Android_12"&gt;not yet released&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Firefox have a &lt;a href="https://hacks.mozilla.org/2020/08/changes-to-samesite-cookie-behavior/"&gt;blog entry from August 2020&lt;/a&gt; which says "Starting with Firefox 79 (June 2020), we rolled it out to 50% of the Firefox Beta user base" - but I've not been able to find any subsequent updates. &lt;strong&gt;Update 26th August 2024:&lt;/strong&gt; It &lt;a href="https://simonwillison.net/2024/Aug/26/frederik-braun/"&gt;turns out&lt;/a&gt; Firefox didn't ship this after all, going with their own &lt;a href="https://blog.mozilla.org/en/mozilla/firefox-rolls-out-total-cookie-protection-by-default-to-all-users-worldwide/"&gt;Total Cookie Protection&lt;/a&gt; solution instead, which rolled out in April 2023.&lt;/li&gt;
&lt;li&gt;I have no idea at all what's going on with Safari!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I started &lt;a href="https://twitter.com/simonw/status/1422366158171238400"&gt;a Twitter thread&lt;/a&gt; to try and collect more information, so please reply there if you know what's going on in more detail.&lt;/p&gt;
&lt;h4 id="chrome-2-minute-twist"&gt;The Chrome 2-minute twist&lt;/h4&gt;
&lt;p&gt;Assuming all of the above, the mystery remained: how did Yan's exploit fail to be prevented by browsers?&lt;/p&gt;
&lt;p&gt;After some back-and-forth about this on Twitter &lt;a href="https://twitter.com/bcrypt/status/1422370774896177154"&gt;Yan proposed&lt;/a&gt; that the answer may be this detail, tucked away on the &lt;a href="https://www.chromestatus.com/feature/5088147346030592"&gt;Chrome Platform Status page for Feature: Cookies default to SameSite=Lax&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Chrome will make an exception for cookies set without a SameSite attribute less than 2 minutes ago. Such cookies will also be sent with non-idempotent (e.g. POST) top-level cross-site requests despite normal SameSite=Lax cookies requiring top-level cross-site requests to have a safe (e.g. GET) HTTP method. Support for this intervention ("Lax + POST") will be removed in the future.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It looks like OkCupid were setting their authentication cookie without a &lt;code&gt;SameSite&lt;/code&gt; attribute... which opened them up to a form-based CSRF attack but only for the 120 seconds following the cookie being set!&lt;/p&gt;
&lt;h4 id="samesite-explore-tool"&gt;Building a tool to explore SameSite browser behaviour&lt;/h4&gt;
&lt;p&gt;I was finding this all very confusing, so I built a tool.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A screenshot showing the two pages from the demo side-by-side" src="https://static.simonwillison.net/static/2021/samesite-tool.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The code lives in &lt;a href="https://github.com/simonw/samesite-lax-demo"&gt;simonw/samesite-lax-demo&lt;/a&gt; on GitHub, but the tool itself has two sides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A server-side Python (&lt;a href="https://www.starlette.io/"&gt;Starlette&lt;/a&gt;) web application for setting cookies with different &lt;code&gt;SameSite&lt;/code&gt; attributes. This is hosted on Vercel at &lt;a href="https://samesite-lax-demo.vercel.app/"&gt;https://samesite-lax-demo.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An HTML page on a different domain that links to that cookied site, provides a POST form targetting it, embeds an image from it and executes some &lt;code&gt;fetch()&lt;/code&gt; requests against it. This is at &lt;a href="https://simonw.github.io/samesite-lax-demo/"&gt;https://simonw.github.io/samesite-lax-demo/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hosting on two separate domains is critical for the tool to show what is going on. I chose Vercel and GitHub Pages because they are both trivial to set up to continuously deploy changes from a GitHub repository.&lt;/p&gt;
&lt;p&gt;Using the tool in different browsers helps show exactly what is going on with regards to cross-domain cookies.&lt;/p&gt;
&lt;p&gt;A few of the things I observed using the tool:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SameSite=Strict&lt;/code&gt; works as you would expect. It's particularly interesting to follow the regular &lt;code&gt;&amp;lt;a href=...&amp;gt;&lt;/code&gt; link from the static site to the application and see how the strict cookie is NOT visible upon arrival - but becomes visible when you refresh that page.&lt;/li&gt;
&lt;li&gt;I included a dynamically generated SVG in a &lt;code&gt;&amp;lt;img src="/cookies.svg"&amp;gt;&lt;/code&gt; image tag, which shows the cookies (using SVG &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt;) that are visible to the request. That image shows all four types of cookie when embedded on the Vercel domain, but when embedded on the GitHub pages domain it differs wildly:
&lt;ul&gt;
&lt;li&gt;Firefox 89 shows both the &lt;code&gt;SameSite=None&lt;/code&gt; and the missing SameSite cookies&lt;/li&gt;
&lt;li&gt;Chrome 92 shows just the &lt;code&gt;SameSite=None&lt;/code&gt; cookie&lt;/li&gt;
&lt;li&gt;Safari 14.0 shows no cookies at all!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Chrome won't let you set a &lt;code&gt;SameSite=None&lt;/code&gt; cookie without including the &lt;code&gt;Secure&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;I also added some JavaScript that makes a cross-domain &lt;code&gt;fetch(..., {credentials: "include"})&lt;/code&gt; call against a &lt;code&gt;/cookies.json&lt;/code&gt; endpoint. This didn't send any cookies at all until I added server-side headers &lt;code&gt;access-control-allow-origin: https://simonw.github.io&lt;/code&gt; and &lt;code&gt;access-control-allow-credentials: true&lt;/code&gt;. Having done that, I got the same results across the three browsers as for the &lt;code&gt;&amp;lt;img&lt;/code&gt; test described above.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Safari ignoring &lt;code&gt;SameSite=None&lt;/code&gt; looked like it was this bug: &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=198181"&gt;Cookies with SameSite=None or SameSite=invalid treated as Strict&lt;/a&gt; - it's marked as fixed but it's not clear to me if the fix has been released yet - I still saw that behaviour on my macOS 10.15.6 laptop or my iOS 14.7.1 iPhone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;  	
&lt;a href="https://news.ycombinator.com/item?id=28092943"&gt;krinchan on Hacker News&lt;/a&gt; has an answer here:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The Safari "bug" is a new setting that's turned on by default: "Prevent cross-site tracking". It treats all cookies as SameSite=Lax, even cookies with SameSite=None.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/"&gt;Full Third-Party Cookie Blocking and More&lt;/a&gt; on the WebKit blog has more about this.&lt;/p&gt;

&lt;p&gt;Most excitingly, I was able to replicate the Chrome two minute window bug using the tool! Each cookie has its value set to the timestamp when it was created, and I added code to display how many seconds ago the cookie was set. Here's an animation showing how Chrome on a form submission navigation can see the cookie that was set with &lt;code&gt;SameSite&lt;/code&gt; missing at 114 seconds old, but that cookie is no longer visible once it passes 120 seconds.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo of the tool in Chrome" src="https://static.simonwillison.net/static/2021/chrome-samesite-missing-loop.gif" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;h4 id="consider-subdomains"&gt;Consider your subdomains&lt;/h4&gt;
&lt;p&gt;One last note about CSRF that you should consider:  &lt;code&gt;SameSite=Lax&lt;/code&gt; still allows form submissions from  subdomains of your primary domain to carry their cookies.&lt;/p&gt;
&lt;p&gt;This means that if you have a XSS vulnerability on one of your subdomains the security of your primary domain will be compromised.&lt;/p&gt;
&lt;p&gt;Since it's common for subdomains to host other applications that may have their own security concerns, ditching CSRF tokens for Lax cookies may not be a wise step!&lt;/p&gt;
&lt;h4 id="login-csrf-samesite-lax"&gt;Login CSRF and SameSite=Lax&lt;/h4&gt;
&lt;p&gt;Login CSRF is an interesting variety of CSRF with slightly different rules.&lt;/p&gt;
&lt;p&gt;A Login CSRF attack is when a malicious forces a user to sign into an account controlled by the attacker. Why do this? Because if that user then saves sensitive information the attacker can see it.&lt;/p&gt;
&lt;p&gt;Imagine I trick you into signing into an e-commerce account I control and saving your credit card details. I could then later sign in myself and buy things on your card!&lt;/p&gt;
&lt;p&gt;Here's how that would work: Say the site's login form makes a POST to &lt;code&gt;https://www.example.com/login&lt;/code&gt; with &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; as the form fields. If those credentials match, the site sets an authentication cookie.&lt;/p&gt;
&lt;p&gt;I can set up my evil website with the following form:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;form&lt;/span&gt; &lt;span class="pl-c1"&gt;action&lt;/span&gt;="&lt;span class="pl-s"&gt;https://www.example.com/login&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;input&lt;/span&gt; &lt;span class="pl-c1"&gt;type&lt;/span&gt;="&lt;span class="pl-s"&gt;hidden&lt;/span&gt;" &lt;span class="pl-c1"&gt;name&lt;/span&gt;="&lt;span class="pl-s"&gt;username&lt;/span&gt;" &lt;span class="pl-c1"&gt;value&lt;/span&gt;="&lt;span class="pl-s"&gt;my-username&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;input&lt;/span&gt; &lt;span class="pl-c1"&gt;type&lt;/span&gt;="&lt;span class="pl-s"&gt;hidden&lt;/span&gt;" &lt;span class="pl-c1"&gt;name&lt;/span&gt;="&lt;span class="pl-s"&gt;password&lt;/span&gt;" &lt;span class="pl-c1"&gt;value&lt;/span&gt;="&lt;span class="pl-s"&gt;my-password&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;form&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;forms&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;submit&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I trick you into visiting my evil pge and you're now signed in to that site using an account that I control. I cross my fingers and hope you don't notice the "you are signed in as X" message in the UI.&lt;/p&gt;
&lt;p&gt;An interesting thing about Login CSRF is that, since it involves setting a cookie but not sending a cookie, &lt;code&gt;SameSite=Lax&lt;/code&gt; would seem to make no difference at all. You need to look to other mechanisms to protect against this attack.&lt;/p&gt;
&lt;p&gt;But actually, you can use &lt;code&gt;SameSite=Lax&lt;/code&gt; to prevent these. The trick is to only allow logins from users that are carrying at least one cookie which you have set in that way - since you know that those cookies could not have been sent if the user originated in a form on another site.&lt;/p&gt;
&lt;p&gt;Another (potentially better) option: check the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin"&gt;HTTP Origin header&lt;/a&gt; on the oncoming request.&lt;/p&gt;
&lt;h4 id="final-recommendations"&gt;Final recommendations&lt;/h4&gt;
&lt;p&gt;As an application developer, you should set all cookies with &lt;code&gt;SameSite=Lax&lt;/code&gt; unless you have a very good reason not to. Most web frameworks do this by default now - Django shipped &lt;a href="https://github.com/django/django/commit/9a56b4b13ed92d2d5bb00d6bdb905a73bc5f2f0a"&gt;support for this&lt;/a&gt; in &lt;a href="https://docs.djangoproject.com/en/3.2/releases/2.1/#requests-and-responses"&gt;Django 2.1&lt;/a&gt; in August 2018.&lt;/p&gt;
&lt;p&gt;Do you still need CSRF tokens as well? I think so: I don't like the idea of users who fire up an older browser (maybe borrowing an obsolete computer) being vulnerable to this attack, and I worry about the subdomain issue described above.&lt;/p&gt;
&lt;p&gt;And if you work for a browser vendor, please make it easier to find information on what the default behaviour is and when it was shipped!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/safari"&gt;safari&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/samesite"&gt;samesite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/starlette"&gt;starlette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="chrome"/><category term="cookies"/><category term="csrf"/><category term="safari"/><category term="security"/><category term="samesite"/><category term="starlette"/></entry><entry><title>Datasette 0.58: The annotated release notes</title><link href="https://simonwillison.net/2021/Jul/16/datasette-058/#atom-tag" rel="alternate"/><published>2021-07-16T02:21:11+00:00</published><updated>2021-07-16T02:21:11+00:00</updated><id>https://simonwillison.net/2021/Jul/16/datasette-058/#atom-tag</id><summary type="html">
    &lt;p&gt;I released &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-58"&gt;Datasette 0.58&lt;/a&gt; last night, with new plugin hooks, Unix domain socket support, a major faceting performance fix and a few other improvements. Here are the &lt;a href="https://simonwillison.net/series/datasette-release-notes/"&gt;annotated release notes&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Faceting performance improvement&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://docs.datasette.io/en/stable/facets.html"&gt;Facets&lt;/a&gt; remains my favourite feature in Datasette: it turns out a simple group by / count against a column is one of the most productive ways I know of to start understanding new data.&lt;/p&gt;
&lt;p&gt;Yesterday I stumbled across &lt;a href="https://github.com/simonw/datasette/commit/a6c8e7fa4cffdeff84e9e755dcff4788fd6154b8"&gt;a tiny tweak&lt;/a&gt; (details in &lt;a href="https://github.com/simonw/datasette/issues/1394"&gt;this issue&lt;/a&gt;) that gave me a 10x performance boost on facet queries! Short version: given the following example query:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  country_long,
  &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;)
&lt;span class="pl-k"&gt;from&lt;/span&gt; (
  &lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; [global&lt;span class="pl-k"&gt;-&lt;/span&gt;power&lt;span class="pl-k"&gt;-&lt;/span&gt;plants]
  &lt;span class="pl-k"&gt;order by&lt;/span&gt; rowid
)
&lt;span class="pl-k"&gt;where&lt;/span&gt;
  country_long &lt;span class="pl-k"&gt;is not null&lt;/span&gt;
&lt;span class="pl-k"&gt;group by&lt;/span&gt;
  country_long
&lt;span class="pl-k"&gt;order by&lt;/span&gt;
  &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;) &lt;span class="pl-k"&gt;desc&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Removing the unnecessary &lt;code&gt;order by rowid&lt;/code&gt; from that inner query knocked the time down from 53ms to 7.2ms (and makes even more of a difference on larger tables).&lt;/p&gt;
&lt;p&gt;I was surprised SQLite didn't perform that optimization automatically - so I &lt;a href="https://sqlite.org/forum/forumpost/2d76f2bcf65d256a"&gt;started a thread&lt;/a&gt; on the SQLite forum and SQLite author D. Richard Hipp &lt;a href="https://sqlite.org/src/timeline?r=omit-subquery-order-by"&gt;figured out a patch&lt;/a&gt;! It's not yet certain that it will land in a SQLite release but I'm excited to have found an issue interesting enough to be worth looking into. (UPDATE: it &lt;a href="https://sqlite.org/forum/forumpost/878ca7a9be0862af?t=h"&gt;landed on trunk&lt;/a&gt;).&lt;/p&gt;
&lt;h4&gt;The get_metadata() plugin hook&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;New plugin hook: &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#plugin-hook-get-metadata"&gt;get_metadata(datasette, key, database, table)&lt;/a&gt;, for returning custom metadata for an instance, database or table. Thanks, Brandon Roberts! (&lt;a href="https://github.com/simonw/datasette/issues/1384"&gt;#1384&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Brandon Roberts contributed this hook as part of work he's been doing with &lt;a href="https://next.newsday.com/"&gt;Newsday nextLI&lt;/a&gt; - always exciting to see Datasette used by another news organization. Brandon has &lt;a href="https://datasette-live.bxroberts.org/"&gt;a live demo&lt;/a&gt; of the plugins he has been building: &lt;a href="https://github.com/next-LI/datasette-live-config"&gt;datasette-live-config&lt;/a&gt;, &lt;a href="https://github.com/next-LI/datasette-live-permissions"&gt;datasette-live-permissions&lt;/a&gt;, &lt;a href="https://github.com/next-LI/datasette-csv-importer"&gt;datasette-csv-importer&lt;/a&gt; and &lt;a href="https://github.com/next-LI/datasette-surveys"&gt;datasette-surveys&lt;/a&gt;. He also has a &lt;a href="https://drive.google.com/file/d/1SShy_C6-CSUlSaqyQSIlUDIa4WA9YzTr/view"&gt;6 minute demo video&lt;/a&gt; explaining the project so far.&lt;/p&gt;
&lt;p&gt;The new hook allows plugins to customize the &lt;a href="https://docs.datasette.io/en/stable/metadata.html#metadata"&gt;metadata&lt;/a&gt; displayed for different databases and tables within the Datasette interface.&lt;/p&gt;
&lt;p&gt;There is one catch at the moment: the plugin doesn't yet allow for async calls (including &lt;code&gt;await db.execute(sql)&lt;/code&gt;) because Datasette's own internals currently treat access to metadata as a sync rather than async feature.&lt;/p&gt;
&lt;p&gt;There are workarounds for this. Brandon's &lt;code&gt;datasette-live-config&lt;/code&gt; plugin &lt;a href="https://github.com/next-LI/datasette-live-config/blob/d7e39db50f33b78ec0ef3f404ba421c4a47a5844/datasette_live_config/__init__.py"&gt;opens an additional, synchronous connection&lt;/a&gt; to the DB which is completely fine for fast queries. Another option would be to keep metadata in an in-memory Python dictionary which is updated by SQL queries that run in an async background task.&lt;/p&gt;
&lt;p&gt;In the longer run though I'd like to redesign Datasette's internals to support asynchronous metadata access - ideally before Datasette 1.0.&lt;/p&gt;
&lt;h4 id="skip-csrf-plugin-hook"&gt;The skip_csrf() plugin hook&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;New plugin hook: &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#plugin-hook-skip-csrf"&gt;skip_csrf(datasette, scope)&lt;/a&gt;, for opting out of CSRF protection based on the incoming request. (&lt;a href="https://github.com/simonw/datasette/issues/1377"&gt;#1377&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wanted to write a plugin that supported an HTTP POST to a Datasette form that wasn't protected by Datasette's &lt;a href="https://docs.datasette.io/en/stable/internals.html?highlight=csrf#csrf-protection"&gt;CSRF protection&lt;/a&gt;. This proved surprisingly difficult! I ended up shipping &lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.9"&gt;asgi-csrf 0.9&lt;/a&gt; with a new mechanism for custom opting-out of CSRF protection based on the ASGI scope, then exposing that mechanism in a new plugin hook in Datasette.&lt;/p&gt;
&lt;p&gt;CSRF is such a frustrating security issue to write code against, because in modern browsers the SameSite cookie attribute more-or-less solves the problem for you... but that attribute only has &lt;a href="https://caniuse.com/same-site-cookie-attribute"&gt;90% global usage according to caniuse.com&lt;/a&gt; - not quite enough for me to forget about it entirely.&lt;/p&gt;
&lt;p&gt;There also remains &lt;a href="https://twitter.com/simonw/status/1413484080226717708"&gt;one obscure edge-case&lt;/a&gt; in which SameSite won't help you: the definition of "same site" includes other subdomains of your domain (provided it's not on the &lt;a href="https://github.com/publicsuffix/list"&gt;Public Suffix List&lt;/a&gt;). This means that for SameSite CSRF protection to work you need to be confident that no subdomains of your domain will suffer an XSS - and in my experience its common for subdomains to be pointed at third-party applications that may not have the same stringent XSS protection as your main code.&lt;/p&gt;
&lt;p&gt;So I continue to care about CSRF protection in Datasette.&lt;/p&gt;
&lt;h4&gt;Unix domain socket support&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;New &lt;code&gt;datasette --uds /tmp/datasette.sock&lt;/code&gt; option for binding Datasette to a Unix domain socket, see &lt;a href="https://docs.datasette.io/en/stable/deploying.html#deploying-proxy"&gt;proxy documentation&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/1388"&gt;#1388&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wrote about this &lt;a href="https://simonwillison.net/2021/Jul/13/unix-domain-sockets/"&gt;in my weeknotes&lt;/a&gt; - this is a great way to run Datasette if you have it behind a proxy such as Apache or nginx and don't want to have the Datasette server listening on a high port.&lt;/p&gt;
&lt;h4&gt;"searchmode": "raw" in table metadata&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;"searchmode": "raw"&lt;/code&gt; table metadata option for defaulting a table to executing SQLite full-text search syntax without first escaping it, see &lt;a href="https://docs.datasette.io/en/stable/full_text_search.html#full-text-search-advanced-queries"&gt;Advanced SQLite search queries&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/1389"&gt;#1389&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;SQLite's built in full-text search feature includes support &lt;a href="https://www.sqlite.org/fts5.html#full_text_query_syntax"&gt;for advanced operators&lt;/a&gt;: you can use operators like AND, OR and NEAR and you can add column specifiers like &lt;code&gt;name:Simon&lt;/code&gt; to restrict searches to individual columns.&lt;/p&gt;
&lt;p&gt;This is something of a two-edged sword: I've found innocent looking queries that raise errors due to unexpected interactions with the query language.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://github.com/simonw/datasette/issues/651"&gt;issue 651&lt;/a&gt; I switched to escaping all queries by default to prevent these errors from happening, with a &lt;code&gt;?_searchmode=raw&lt;/code&gt; query string option for opting back into the default functionality.&lt;/p&gt;
&lt;p&gt;I've since had a few requests for a mechanism to enable this by default - hence the new &lt;code&gt;"searchmode": "raw"&lt;/code&gt; option in table metadata.&lt;/p&gt;
&lt;h4&gt;Link plugin hooks now take a request&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#plugin-hook-menu-links"&gt;menu_links()&lt;/a&gt;, &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#plugin-hook-table-actions"&gt;table_actions()&lt;/a&gt; and &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#plugin-hook-database-actions"&gt;database_actions()&lt;/a&gt; plugin hooks all gained a new optional &lt;code&gt;request&lt;/code&gt; argument providing access to the current request. (&lt;a href="https://github.com/simonw/datasette/issues/1371"&gt;#1371&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I have a plugin which needs to add links to different places depending on the subdomain that the Datasette instance is running on. Adding &lt;code&gt;request&lt;/code&gt; to these plugin hooks proved to be the easiest way to achieve this.&lt;/p&gt;
&lt;p&gt;This is a really nice thing about how &lt;a href="https://pluggy.readthedocs.io/"&gt;Pluggy&lt;/a&gt; (the plugin library used by Datasette) works: adding new named parameters to hooks can be done without breaking backwards compatibility with existing plugins.&lt;/p&gt;
&lt;h4&gt;And the rest&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Improved documentation for &lt;a href="https://docs.datasette.io/en/stable/deploying.html#deploying-proxy"&gt;Running Datasette behind a proxy&lt;/a&gt; to recommend using &lt;code&gt;ProxyPreservehost On&lt;/code&gt; with Apache. (&lt;a href="https://github.com/simonw/datasette/issues/1387"&gt;#1387&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST&lt;/code&gt; requests to endpoints that do not support that HTTP verb now return a 405 error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db.path&lt;/code&gt; can now be provided as a &lt;code&gt;pathlib.Path&lt;/code&gt; object, useful when writing unit tests for plugins. Thanks, Chris Amico. (&lt;a href="https://github.com/simonw/datasette/issues/1365"&gt;#1365&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releasenotes"&gt;releasenotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/d-richard-hipp"&gt;d-richard-hipp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/samesite"&gt;samesite&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="releasenotes"/><category term="sqlite"/><category term="datasette"/><category term="annotated-release-notes"/><category term="d-richard-hipp"/><category term="samesite"/></entry><entry><title>2020 Web Milestones</title><link href="https://simonwillison.net/2020/Jan/24/2020-web-milestones/#atom-tag" rel="alternate"/><published>2020-01-24T04:43:16+00:00</published><updated>2020-01-24T04:43:16+00:00</updated><id>https://simonwillison.net/2020/Jan/24/2020-web-milestones/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://mike.sherov.com/2020-web-milestones/"&gt;2020 Web Milestones&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A lot of stuff is happening in 2020! Mike Sherov rounds it up—highlights include the release of Chromium Edge (Microsoft’s Chrome-powered browser for Windows 7+), Web Components supported in every major browser, Deno 1.x, SameSite Cookies turned on by default (which should dramatically reduce CSRF exposure) and Python 2 and Flash EOLs.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mikesherov/status/1220562604348846081"&gt;@mikesherov&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flash"&gt;flash&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/internet-explorer"&gt;internet-explorer&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web"&gt;web&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/samesite"&gt;samesite&lt;/a&gt;&lt;/p&gt;



</summary><category term="chrome"/><category term="csrf"/><category term="flash"/><category term="internet-explorer"/><category term="javascript"/><category term="python"/><category term="web"/><category term="deno"/><category term="samesite"/></entry><entry><title>Quoting Troy Hunt</title><link href="https://simonwillison.net/2020/Jan/3/troy-hunt/#atom-tag" rel="alternate"/><published>2020-01-03T16:22:42+00:00</published><updated>2020-01-03T16:22:42+00:00</updated><id>https://simonwillison.net/2020/Jan/3/troy-hunt/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://www.troyhunt.com/promiscuous-cookies-and-their-impending-death-via-the-samesite-policy/"&gt;&lt;p&gt;Come version 80, any cookie without a SameSite attribute will be treated as "Lax" by Chrome. This is really important to understand because put simply, it'll very likely break a bunch of stuff. [...] The fix is easy, all it needs is for everyone responsible for maintaining any system that uses cookies that might be passed from an external origin to understand what's going on. Can't be that hard, right? Hello? Oh...&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://www.troyhunt.com/promiscuous-cookies-and-their-impending-death-via-the-samesite-policy/"&gt;Troy Hunt&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/samesite"&gt;samesite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/troy-hunt"&gt;troy-hunt&lt;/a&gt;&lt;/p&gt;



</summary><category term="chrome"/><category term="cookies"/><category term="csrf"/><category term="samesite"/><category term="troy-hunt"/></entry></feed>