<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: csrf</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/csrf.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-04-14T23:58:53+00:00</updated><author><name>Simon Willison</name></author><entry><title>datasette PR #2689: Replace token-based CSRF with Sec-Fetch-Site header protection</title><link href="https://simonwillison.net/2026/Apr/14/replace-token-based-csrf/#atom-tag" rel="alternate"/><published>2026-04-14T23:58:53+00:00</published><updated>2026-04-14T23:58:53+00:00</updated><id>https://simonwillison.net/2026/Apr/14/replace-token-based-csrf/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette/pull/2689"&gt;datasette PR #2689: Replace token-based CSRF with Sec-Fetch-Site header protection&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Datasette has long protected against CSRF attacks using CSRF tokens, implemented using my &lt;a href="https://github.com/simonw/asgi-csrf"&gt;asgi-csrf&lt;/a&gt; Python library. These are something of a pain to work with - you need to scatter forms in templates with &lt;code&gt;&amp;lt;input type="hidden" name="csrftoken" value="{{ csrftoken() }}"&amp;gt;&lt;/code&gt; lines and then selectively disable CSRF protection for APIs that are intended to be called from outside the browser.&lt;/p&gt;
&lt;p&gt;I've been following Filippo Valsorda's research here with interest, described in &lt;a href="https://words.filippo.io/csrf/"&gt;this detailed essay from August 2025&lt;/a&gt; and shipped &lt;a href="https://tip.golang.org/doc/go1.25#nethttppkgnethttp"&gt;as part of Go 1.25&lt;/a&gt; that same month.&lt;/p&gt;
&lt;p&gt;I've now landed the same change in Datasette. Here's the PR description - Claude Code did much of the work (across 10 commits, closely guided by me and cross-reviewed by GPT-5.4) but I've decided to start writing these PR descriptions by hand, partly to make them more concise and also as an exercise in keeping myself honest.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New CSRF protection middleware inspired by Go 1.25 and &lt;a href="https://words.filippo.io/csrf/"&gt;this research&lt;/a&gt; by Filippo Valsorda. This replaces the old CSRF token based protection.&lt;/li&gt;
&lt;li&gt;Removes all instances of &lt;code&gt;&amp;lt;input type="hidden" name="csrftoken" value="{{ csrftoken() }}"&amp;gt;&lt;/code&gt; in the templates - they are no longer needed.&lt;/li&gt;
&lt;li&gt;Removes the &lt;code&gt;def skip_csrf(datasette, scope):&lt;/code&gt; plugin hook defined in &lt;code&gt;datasette/hookspecs.py&lt;/code&gt; and its documentation and tests.&lt;/li&gt;
&lt;li&gt;Updated &lt;a href="https://docs.datasette.io/en/latest/internals.html#csrf-protection"&gt;CSRF protection documentation&lt;/a&gt; to describe the new approach.&lt;/li&gt;
&lt;li&gt;Upgrade guide now &lt;a href="https://docs.datasette.io/en/latest/upgrade_guide.html#csrf-protection-is-now-header-based"&gt;describes the CSRF change&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="security"/><category term="datasette"/><category term="ai-assisted-programming"/></entry><entry><title>A modern approach to preventing CSRF in Go</title><link href="https://simonwillison.net/2025/Oct/15/csrf-in-go/#atom-tag" rel="alternate"/><published>2025-10-15T05:03:46+00:00</published><updated>2025-10-15T05:03:46+00:00</updated><id>https://simonwillison.net/2025/Oct/15/csrf-in-go/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.alexedwards.net/blog/preventing-csrf-in-go"&gt;A modern approach to preventing CSRF in Go&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Alex Edwards writes about the new &lt;code&gt;http.CrossOriginProtection&lt;/code&gt; middleware that was added to the Go standard library in &lt;a href="https://tip.golang.org/doc/go1.25"&gt;version 1.25&lt;/a&gt; in August and asks:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Have we finally reached the point where CSRF attacks can be prevented without relying on a token-based check (like double-submit cookies)?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It looks like the answer might be &lt;em&gt;yes&lt;/em&gt;, which is extremely exciting. I've been &lt;a href="https://simonwillison.net/tags/csrf/"&gt;tracking CSRF&lt;/a&gt; since I first learned about it &lt;a href="https://simonwillison.net/2005/May/6/bad/"&gt;20 years ago in May 2005&lt;/a&gt; and a cleaner solution than those janky hidden form fields would be very welcome.&lt;/p&gt;
&lt;p&gt;The code for the new Go middleware lives in &lt;a href="https://github.com/golang/go/blob/go1.25.0/src/net/http/csrf.go"&gt;src/net/http/csrf.go&lt;/a&gt;. It works using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site"&gt;Sec-Fetch-Site&lt;/a&gt; HTTP header, which Can I Use shows as having &lt;a href="https://caniuse.com/mdn-http_headers_sec-fetch-site"&gt;94.18%&lt;/a&gt; global availability - the holdouts are mainly IE11, iOS versions prior to iOS 17 (which came out in 2023 but can be installed on any phone released since 2017) and some other ancient browser versions.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;Sec-Fetch-Site&lt;/code&gt; is &lt;code&gt;same-origin&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt; then the page submitting the form was either on the same origin or was navigated to directly by the user - in both cases safe from CSRF. If it's &lt;code&gt;cross-site&lt;/code&gt; or &lt;code&gt;same-site&lt;/code&gt; (&lt;code&gt;tools.simonwillison.net&lt;/code&gt; and &lt;code&gt;til.simonwillison.net&lt;/code&gt; are considered &lt;code&gt;same-site&lt;/code&gt; but not &lt;code&gt;same-origin&lt;/code&gt;) the submission is denied.&lt;/p&gt;
&lt;p&gt;If that header isn't available the middleware falls back on comparing other headers: &lt;code&gt;Origin&lt;/code&gt; - a value like &lt;code&gt;https://simonwillison.net&lt;/code&gt; - with &lt;code&gt;Host&lt;/code&gt;, a value like &lt;code&gt;simonwillison.net&lt;/code&gt;. This should cover the tiny fraction of browsers that don't have the new header, though it's not clear to me if there are any weird edge-cases beyond that.&lt;/p&gt;
&lt;p&gt;Note that this fallback comparison can't take the scheme into account since &lt;code&gt;Host&lt;/code&gt; doesn't list that, so administrators are encouraged to use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security"&gt;HSTS&lt;/a&gt; to protect against HTTP to HTTPS cross-origin requests.&lt;/p&gt;
&lt;p&gt;On Lobste.rs I questioned if this would work for &lt;code&gt;localhost&lt;/code&gt;, since that normally isn't served using HTTPS. Firefox security engineer Frederik Braun &lt;a href="https://lobste.rs/s/fzw9g7/modern_approach_preventing_csrf_go#c_e24o9q"&gt;reassured me&lt;/a&gt; that &lt;code&gt;*.localhost&lt;/code&gt; is treated as a Secure Context, so gets the &lt;code&gt;Sec-Fetch-Site&lt;/code&gt; header despite not being served via HTTPS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Also relevant is &lt;a href="https://words.filippo.io/csrf/"&gt;Filippo Valsorda's article in CSRF&lt;/a&gt; which includes detailed research conducted as part of building the new Go middleware, plus this related &lt;a href="https://bsky.app/profile/filippo.abyssdomain.expert/post/3lmyu7c25zq2o"&gt;Bluesky conversation&lt;/a&gt; about that research from six months ago.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/fzw9g7/modern_approach_preventing_csrf_go"&gt;lobste.rs&lt;/a&gt;&lt;/small&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/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/filippo-valsorda"&gt;filippo-valsorda&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="csrf"/><category term="go"/><category term="security"/><category term="filippo-valsorda"/></entry><entry><title>Maintainers of Last Resort</title><link href="https://simonwillison.net/2025/Aug/16/maintainers-of-last-resort/#atom-tag" rel="alternate"/><published>2025-08-16T16:52:45+00:00</published><updated>2025-08-16T16:52:45+00:00</updated><id>https://simonwillison.net/2025/Aug/16/maintainers-of-last-resort/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://words.filippo.io/last-resort/"&gt;Maintainers of Last Resort&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Filippo Valsorda founded Geomys &lt;a href="https://simonwillison.net/2024/Jul/8/geomys/"&gt;last year&lt;/a&gt; as an "organization of professional open source maintainers", providing maintenance and support for critical packages in the Go language ecosystem backed by clients in retainer relationships. &lt;/p&gt;
&lt;p&gt;This is an inspiring and optimistic shape for financially sustaining key open source projects, and it appears be working really well.&lt;/p&gt;
&lt;p&gt;Most recently, Geomys have started acting as a "maintainer of last resort" for security-related Go projects in need of new maintainers. In this piece Filippo describes their work on the &lt;a href="https://github.com/microcosm-cc/bluemonday"&gt;bluemonday&lt;/a&gt; HTML sanitization library - similar to Python’s bleach which was &lt;a href="https://github.com/mozilla/bleach/issues/698"&gt;deprecated in 2023&lt;/a&gt;. He also talks at length about their work on CSRF for Go after &lt;a href="https://github.com/gorilla/csrf"&gt;gorilla/csrf&lt;/a&gt; lost active maintenance - I’m still working my way through his earlier post on &lt;a href="https://words.filippo.io/csrf/"&gt;Cross-Site Request Forgery&lt;/a&gt; trying to absorb the research shared their about the best modern approaches to this vulnerability.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/4hc7o5/maintainers_last_resort"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&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/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/filippo-valsorda"&gt;filippo-valsorda&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="go"/><category term="open-source"/><category term="security"/><category term="filippo-valsorda"/></entry><entry><title>GitHub OAuth for a static site using Cloudflare Workers</title><link href="https://simonwillison.net/2024/Nov/29/github-oauth-cloudflare/#atom-tag" rel="alternate"/><published>2024-11-29T18:13:18+00:00</published><updated>2024-11-29T18:13:18+00:00</updated><id>https://simonwillison.net/2024/Nov/29/github-oauth-cloudflare/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/cloudflare/workers-github-oauth"&gt;GitHub OAuth for a static site using Cloudflare Workers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's a TIL covering a Thanksgiving AI-assisted programming project. I wanted to add OAuth against GitHub to some of the projects on my &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; site in order to implement "Save to Gist".&lt;/p&gt;
&lt;p&gt;That site is entirely statically hosted by GitHub Pages, but OAuth has a required server-side component: there's a &lt;code&gt;client_secret&lt;/code&gt; involved that should never be included in client-side code.&lt;/p&gt;
&lt;p&gt;Since I serve the site from behind Cloudflare I realized that a minimal &lt;a href="https://workers.cloudflare.com/"&gt;Cloudflare Workers&lt;/a&gt; script may be enough to plug the gap. I got Claude on my phone to build me a prototype and then pasted that (still on my phone) into a new Cloudflare Worker and it worked!&lt;/p&gt;
&lt;p&gt;... almost. On later closer inspection of the code it was missing error handling... and then someone pointed out it was vulnerable to a login CSRF attack thanks to failure to check the &lt;code&gt;state=&lt;/code&gt; parameter. I worked with Claude to fix those too.&lt;/p&gt;
&lt;p&gt;Useful reminder here that pasting code AI-generated code around on a mobile phone isn't necessarily the best environment to encourage a thorough code review!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/oauth"&gt;oauth&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="github"/><category term="oauth"/><category term="projects"/><category term="security"/><category term="tools"/><category term="ai"/><category term="cloudflare"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>Datasette 1.0a15</title><link href="https://simonwillison.net/2024/Aug/16/datasette-10a15/#atom-tag" rel="alternate"/><published>2024-08-16T05:06:51+00:00</published><updated>2024-08-16T05:06:51+00:00</updated><id>https://simonwillison.net/2024/Aug/16/datasette-10a15/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.datasette.io/en/latest/changelog.html#a15-2024-08-15"&gt;Datasette 1.0a15&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Mainly bug fixes, but a couple of minor new features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Datasette now defaults to hiding SQLite "shadow" tables, as seen in extensions such as SQLite FTS and &lt;a href="https://github.com/asg017/sqlite-vec"&gt;sqlite-vec&lt;/a&gt;. Virtual tables that it makes sense to display, such as FTS core tables, are no longer hidden. Thanks, &lt;a href="https://github.com/asg017"&gt;Alex Garcia&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/2296"&gt;#2296&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The Datasette homepage is now duplicated at &lt;code&gt;/-/&lt;/code&gt;, using the default &lt;code&gt;index.html&lt;/code&gt; template. This ensures that the information on that page is still accessible even if the Datasette homepage has been customized using a custom &lt;code&gt;index.html&lt;/code&gt; template, for example on sites like &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/2393"&gt;#2393&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Datasette also now &lt;a href="https://github.com/simonw/datasette/issues/2390"&gt;serves more user-friendly CSRF pages&lt;/a&gt;, an improvement which required me to ship &lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.10"&gt;asgi-csrf 0.10&lt;/a&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/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releases"&gt;releases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="projects"/><category term="releases"/><category term="datasette"/></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/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="security"/><category term="samesite"/><category term="starlette"/></entry><entry><title>OkCupid had a CSRF vulnerability</title><link href="https://simonwillison.net/2021/Aug/2/okcupid-had-a-csrf-vulnerability/#atom-tag" rel="alternate"/><published>2021-08-02T22:12:36+00:00</published><updated>2021-08-02T22:12:36+00:00</updated><id>https://simonwillison.net/2021/Aug/2/okcupid-had-a-csrf-vulnerability/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.azuki.vip/csrf/"&gt;OkCupid had a CSRF vulnerability&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Good write-up of a (now fixed) CSRF vulnerability on OkCupid. Their site worked by POSTing JSON objects to an API. JSON POSTs are usually protected against CSRF because they can only be sent using &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt;, which are protected by the same-origin policy. Yan Zhu notes that you can use the &lt;code&gt;enctype="text/plain"&lt;/code&gt; attribute on a form (introduced in HTML5) and a crafty hidden input element with &lt;code&gt;name='{"foo":"' value='bar"}'&lt;/code&gt; to construct JSON in an off-site form, which enabled CSRF attacks.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=28039631"&gt;How to boost your popularity on OkCupid using CSRF and a JSON type confusion on Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&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/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="security"/></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>Weeknotes: sqlite-utils updates, Datasette and asgi-csrf, open-sourcing VIAL</title><link href="https://simonwillison.net/2021/Jun/28/weeknotes/#atom-tag" rel="alternate"/><published>2021-06-28T17:23:21+00:00</published><updated>2021-06-28T17:23:21+00:00</updated><id>https://simonwillison.net/2021/Jun/28/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;Some work on &lt;code&gt;sqlite-utils&lt;/code&gt;, &lt;code&gt;asgi-csrf&lt;/code&gt;, a Datasette alpha and we open-sourced VIAL.&lt;/p&gt;
&lt;h4&gt;sqlite-utils&lt;/h4&gt;
&lt;p&gt;Last week's &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-10"&gt;sqlite-utils 3.10&lt;/a&gt; introduced a huge new feature: the ability to &lt;a href="https://simonwillison.net/2021/Jun/19/sqlite-utils-memory/"&gt;run joins directly against CSV and JSON files from the command-line&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've since released &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#v3-11"&gt;sqlite-utils 3.11&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#v3-12"&gt;3.12&lt;/a&gt;, much smaller releases.&lt;/p&gt;
&lt;p&gt;3.11 added a new &lt;code&gt;--schema&lt;/code&gt; option to the &lt;code&gt;sqlite-utils memory&lt;/code&gt; command which lets you see the schema you'll be querying for the imported data:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl 'https://api.github.com/users/dogsheep/repos' | \
  sqlite-utils memory - --schema
CREATE TABLE [stdin] (
   [id] INTEGER,
   [node_id] TEXT,
   [name] TEXT,
   [full_name] TEXT,
   [private] INTEGER,
   [owner] TEXT,
   [html_url] TEXT,
   [description] TEXT,
   ...
   [watchers] INTEGER,
   [default_branch] TEXT
);
CREATE VIEW t1 AS select * from [stdin];
CREATE VIEW t AS select * from [stdin];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.12 focused on the Python library side of the package. It adds a new method, &lt;code&gt;db.query(sql)&lt;/code&gt; which returns an iterator over Python dictionaries representing the results of a query.&lt;/p&gt;
&lt;p&gt;This was a pretty obvious missing feature of the library: the rest of &lt;code&gt;sqlite-utils&lt;/code&gt; deals with rows that are represented as dictionaries - you pass a list of Python dictionaries to &lt;code&gt;db[table_name].insert_all(list_of_dicts)&lt;/code&gt; to create a table with the correct schema, for example. But if you wanted to execute &lt;code&gt;SELECT&lt;/code&gt; queries you had to use &lt;code&gt;db.execute()&lt;/code&gt; which would return a standard library cursor object which could then return tuples if you called &lt;code&gt;.fetchall()&lt;/code&gt; on it.&lt;/p&gt;
&lt;p&gt;It was only when I started to work on an interactive Jupyter notebook tutorial for &lt;code&gt;sqlite-utils&lt;/code&gt; that I realized how weird it was not to have an equivalent method for reading data out of the database again.&lt;/p&gt;
&lt;p&gt;Here's what the new method looks like:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Database&lt;/span&gt;(&lt;span class="pl-s1"&gt;memory&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;].&lt;span class="pl-en"&gt;insert_all&lt;/span&gt;([
    {&lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Cleo"&lt;/span&gt;},
    {&lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Pancakes"&lt;/span&gt;}
])
&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;row&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;query&lt;/span&gt;(&lt;span class="pl-s"&gt;"select * from dogs"&lt;/span&gt;):
    &lt;span class="pl-en"&gt;print&lt;/span&gt;(&lt;span class="pl-s1"&gt;row&lt;/span&gt;)
&lt;span class="pl-c"&gt;# Outputs:&lt;/span&gt;
&lt;span class="pl-c"&gt;# {'name': 'Cleo'}&lt;/span&gt;
&lt;span class="pl-c"&gt;# {'name': 'Pancakes'}&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#db-query-sql-params"&gt;Full documentation here&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;asgi-csrf and a Datasette alpha&lt;/h4&gt;
&lt;p&gt;I'm building a custom Datasette integration for a consulting client at the moment which needs to be able to accept &lt;code&gt;POST&lt;/code&gt; form data as part of an API. Datasette has &lt;a href="https://docs.datasette.io/en/stable/internals.html#csrf-protection"&gt;CSRF protection&lt;/a&gt; but for this particular project I need to opt-out of that protection for this one endpoint.&lt;/p&gt;
&lt;p&gt;I ended up releasing &lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.9"&gt;asgi-csrf 0.9&lt;/a&gt; with a new &lt;code&gt;skip_if_scope=&lt;/code&gt; mechanism for dynamically disabling CSRF protection based on the incoming ASGI scope. I then shipped a &lt;a href="https://github.com/simonw/datasette/releases/tag/0.58a1"&gt;Datasette 0.58a1&lt;/a&gt; alpha release with a new &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-hook-skip-csrf"&gt;skip_csrf(datasette, scope)&lt;/a&gt; plugin hook for plugins to take advantage of that mechanism.&lt;/p&gt;
&lt;p&gt;Expect another alpha release shortly to preview the new &lt;a href="https://github.com/simonw/datasette/issues/1384"&gt;get_metadata plugin hook&lt;/a&gt; contributed by Brandon Roberts. I've decided that alphas are the ideal way to explore new plugin hooks while they are still being developed as it lets projects &lt;code&gt;pip install&lt;/code&gt; the alpha while making it clear that the interface may not yet be fully baked.&lt;/p&gt;
&lt;h4&gt;Open-sourcing VIAL&lt;/h4&gt;
&lt;p&gt;VIAL is the project I've been working on for VaccinateCA/VaccinateTheStates - see &lt;a href="https://simonwillison.net/tags/vaccinateca/"&gt;previous posts&lt;/a&gt;. It's a Django application which powers a crowd-sourced and scraper-driven effort to catalogue all of the places in the USA that you can get the Covid vaccine - 77,000 and counting right now.&lt;/p&gt;
&lt;p&gt;We had always intended to open-source the code and now we have! &lt;a href="https://github.com/CAVaccineInventory/vial"&gt;github.com/CAVaccineInventory/vial&lt;/a&gt; is the newly-made-public repository.&lt;/p&gt;
&lt;p&gt;I still need to produce a bunch of extra documentation about VIAL, likely including a video introduction to the project. But it's great to have it out there!&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.12"&gt;3.12&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;80 releases total&lt;/a&gt;) - 2021-06-25
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.58a1"&gt;0.58a1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette/releases"&gt;92 releases total&lt;/a&gt;) - 2021-06-24
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/asgi-csrf"&gt;asgi-csrf&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.9"&gt;0.9&lt;/a&gt; - (&lt;a href="https://github.com/simonw/asgi-csrf/releases"&gt;17 releases total&lt;/a&gt;) - 2021-06-23
&lt;br /&gt;ASGI middleware for protecting against CSRF attacks&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/reddit/scraping-reddit-json"&gt;Scraping Reddit via their JSON API&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/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccinate-ca"&gt;vaccinate-ca&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="datasette"/><category term="asgi"/><category term="weeknotes"/><category term="sqlite-utils"/><category term="vaccinate-ca"/></entry><entry><title>Weeknotes: Rocky Beaches, Datasette 0.48, a commit history of my database</title><link href="https://simonwillison.net/2020/Aug/21/weeknotes-rocky-beaches/#atom-tag" rel="alternate"/><published>2020-08-21T00:52:16+00:00</published><updated>2020-08-21T00:52:16+00:00</updated><id>https://simonwillison.net/2020/Aug/21/weeknotes-rocky-beaches/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I helped Natalie launch &lt;a href="https://www.rockybeaches.com/"&gt;Rocky Beaches&lt;/a&gt;, shipped Datasette 0.48 and several releases of &lt;code&gt;datasette-graphql&lt;/code&gt;, upgraded the CSRF protection for &lt;code&gt;datasette-upload-csvs&lt;/code&gt; and figured out how to get a commit log of changes to my blog by backing up its database to a GitHub repository.&lt;/p&gt;
&lt;h4 id="rocky-beaches"&gt;Rocky Beaches&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://twitter.com/natbat"&gt;Natalie&lt;/a&gt; released the first version of &lt;a href="https://www.rockybeaches.com/"&gt;rockybeaches.com&lt;/a&gt; this week. It's a site that helps you find places to go tidepooling (known as rockpooling in the UK) and figure out the best times to go based on low tide times.&lt;/p&gt;

&lt;p&gt;&lt;img style="max-width: 100%" src="https://static.simonwillison.net/static/2020/Rocky_Beaches__Pillar_Point_Harbor_CA.jpg" alt="Screenshot of the Pillar Point page for Rocky Beaches" /&gt;&lt;/p&gt;

&lt;p&gt;I helped out with the backend for the site, mainly as an excuse to further explore the idea of using Datasette to power full websites (previously explored with &lt;a href="https://simonwillison.net/2019/Nov/25/niche-museums/"&gt;Niche Museums&lt;/a&gt; and &lt;a href="https://simonwillison.net/2020/Apr/20/self-rewriting-readme/"&gt;my TILs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The site uses a pattern I've been really enjoying: it's essentially a static dynamic site. Pages are dynamically rendered by Datasette using Jinja templates and a SQLite database, but the database itself is treated as a static asset: it's built at deploy time by &lt;a href="https://github.com/natbat/rockybeaches/blob/main/.github/workflows/deploy.yml"&gt;this GitHub Actions workflow&lt;/a&gt; and deployed (currently to &lt;a href="https://www.vercel.com/"&gt;Vercel&lt;/a&gt;) as a binary asset along with the code.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/natbat/rockybeaches/blob/main/scripts/build.sh"&gt;build script&lt;/a&gt; uses &lt;a href="https://github.com/simonw/yaml-to-sqlite"&gt;yaml-to-sqlite&lt;/a&gt; to load two YAML files - &lt;a href="https://github.com/natbat/rockybeaches/blob/4127c0f0539178664cefed4aca00db2b5c00c855/data/places.yml"&gt;places.yml&lt;/a&gt; and &lt;a href="https://github.com/natbat/rockybeaches/blob/4127c0f0539178664cefed4aca00db2b5c00c855/data/stations.yml"&gt;stations.yml&lt;/a&gt; - and create the &lt;code&gt;stations&lt;/code&gt; and &lt;code&gt;places&lt;/code&gt; database tables.&lt;/p&gt;
&lt;p&gt;It then runs two custom Python scripts to fetch relevant data for those places from &lt;a href="https://www.inaturalist.org/"&gt;iNaturalist&lt;/a&gt; and the &lt;a href="https://tidesandcurrents.noaa.gov/web_services_info.html"&gt;NOAA Tides &amp;amp; Currents API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The data all ends up in the Datasette instance that powers the site - you can browse it at &lt;a href="http://www.rockybeaches.com/data"&gt;www.rockybeaches.com/data&lt;/a&gt; or interact with it using GraphQL API at &lt;a href="http://www.rockybeaches.com/graphql"&gt;www.rockybeaches.com/graphql&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The code is a little convoluted at the moment - I'm still iterating towards the best patterns for building websites like this using Datasette - but I'm very pleased with the productivity and performance that this approach produced.&lt;/p&gt;
&lt;h4 id="datasette-048"&gt;Datasette 0.48&lt;/h4&gt;
&lt;p&gt;Highlights from &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-48"&gt;Datasette 0.48&lt;/a&gt; release notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Datasette documentation now lives at &lt;a href="https://docs.datasette.io/"&gt;docs.datasette.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;extra_template_vars&lt;/code&gt;, &lt;code&gt;extra_css_urls&lt;/code&gt;, &lt;code&gt;extra_js_urls&lt;/code&gt; and &lt;code&gt;extra_body_script&lt;/code&gt; plugin hooks now all accept the same arguments. See &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#plugin-hook-extra-template-vars"&gt;extra_template_vars(template, database, table, columns, view_name, request, datasette)&lt;/a&gt; for details. (&lt;a href="https://github.com/simonw/datasette/issues/939"&gt;#939&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Those hooks now accept a new &lt;code&gt;columns&lt;/code&gt; argument detailing the table columns that will be rendered on that page. (&lt;a href="https://github.com/simonw/datasette/issues/938"&gt;#938&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I released a new version of &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; that takes advantage of the new &lt;code&gt;columns&lt;/code&gt; argument to only inject Leaflet maps JavaScript onto the page if the table being rendered includes latitude and longitude columns - previously the plugin would load extra code on pages that weren't going to render a map at all. That's now running on &lt;a href="https://global-power-plants.datasettes.com/"&gt;https://global-power-plants.datasettes.com/&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="datasette-graphql"&gt;datasette-graphql&lt;/h4&gt;
&lt;p&gt;Using &lt;a href="https://github.com/simonw/datasette-graphql"&gt;datasette-graphql&lt;/a&gt; for Rocky Beaches inspired me to add two new features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new &lt;code&gt;graphql()&lt;/code&gt; Jinja custom template function that lets you execute custom GraphQL queries inside a Datasette template page - which turns out to be a pretty elegant way for the template to load exactly the data that it needs in order to render the page. Here's &lt;a href="https://github.com/natbat/rockybeaches/blob/70039f18b3d3823a4f069deca513e950a3aaba4f/templates/row-data-places.html#L29-L46"&gt;how Rocky Beaches uses that&lt;/a&gt;. &lt;a href="https://github.com/simonw/datasette-graphql/issues/50"&gt;Issue 50&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Some of the iNaturalist data that Rocky Beaches uses is stored as JSON data in text columns in SQLite - mainly because I was too lazy to model it out as tables. This was coming out of the GraphQL API as strings-containing-JSON, so I added a &lt;code&gt;json_columns&lt;/code&gt; plugin configuration mechanism for turning those into Graphene &lt;code&gt;GenericScalar&lt;/code&gt; fields - see &lt;a href="https://github.com/simonw/datasette-graphql/issues/53"&gt;issue 53&lt;/a&gt; for details.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also landed a big performance improvement. The plugin works by introspecting the database and generating a GraphQL schema that represents those tables, columns and views. For tables with a lot of tables this can get expensive, and the introspection was being run on every request.&lt;/p&gt;
&lt;p&gt;I didn't want to require a server restart any time the schema changed, so I didn't want to cache the schema in-memory. Ideally it would be cached but the cache would become invalid any time the schema itself changed.&lt;/p&gt;
&lt;p&gt;It turns out SQLite has a mechanism for this: the &lt;code&gt;PRAGMA schema_version&lt;/code&gt; statement, which returns an integer version number that changes any time the underlying schema is changed (e.g. a table is added or modified).&lt;/p&gt;
&lt;p&gt;I built a quick &lt;a href="https://github.com/simonw/datasette-schema-versions"&gt;datasette-schema-versions&lt;/a&gt; plugin to try this feature out (in less than twenty minutes thanks to my &lt;a href="https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/"&gt;datasette-plugin cookiecutter template&lt;/a&gt;) and prove to myself that it works. Then I built a caching mechanism for &lt;code&gt;datasette-graphql&lt;/code&gt; that uses the current &lt;code&gt;schema_version&lt;/code&gt; as the cache key. See &lt;a href="https://github.com/simonw/datasette-graphql/issues/51"&gt;issue 51&lt;/a&gt; for details.&lt;/p&gt;
&lt;h4 id="asgi-csrf-and-datasette-upload-csvs"&gt;asgi-csrf and datasette-upload-csvs&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-upload-csvs"&gt;datasette-upload-csvs&lt;/a&gt; is a Datasette plugin that adds a form for uploading CSV files and converting them to SQLite tables.&lt;/p&gt;
&lt;p&gt;Datasette 0.44 &lt;a href="https://docs.datasette.io/en/latest/changelog.html#csrf-protection"&gt;added CSRF protection&lt;/a&gt;, which broke the plugin. I fixed that this week, but it took some extra work because file uploads use the &lt;code&gt;multipart/form-data&lt;/code&gt; HTTP mechanism and my &lt;a href="https://github.com/simonw/asgi-csrf"&gt;asgi-csrf&lt;/a&gt; library didn't support that.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://github.com/simonw/asgi-csrf/issues/1"&gt;fixed that&lt;/a&gt; this week, but the code was quite complicated. Since &lt;code&gt;asgi-csrf&lt;/code&gt; is a security library I decided to aim for 100% code coverage, the first time I've done that for one of my projects.&lt;/p&gt;
&lt;p&gt;I got there with the help of codecov.io and &lt;a href="https://pypi.org/project/pytest-cov/"&gt;pytest-cov&lt;/a&gt;. I wrote up what I learned about those tools in &lt;a href="https://github.com/simonw/til/blob/main/pytest/pytest-code-coverage.md"&gt;a TIL&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="backing-up-my-blog-database-to-a-github-repository"&gt;Backing up my blog database to a GitHub repository&lt;/h4&gt;
&lt;p&gt;I really like keeping content in a git repository (see Rocky Beaches and Niche Museums). Every content management system I've ever been has eventually desired revision control, and modeling that in a database and adding it to an existing project is always a huge pain.&lt;/p&gt;
&lt;p&gt;I have 18 years of content on this blog. I want that backed up to git - and this week I realized I have the tools to do that already.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/db-to-sqlite"&gt;db-to-sqlite&lt;/a&gt; is my tool for taking any SQL Alchemy supported database (so far tested with MySQL and PostgreSQL) and exporting it into a SQLite database.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/sqlite-diffable"&gt;sqlite-diffable&lt;/a&gt; is a very early stage tool I built last year. The idea is to dump a SQLite database out to disk in a way that is designed to work well with git diffs. Each table is dumped out as newline-delimited JSON, one row per line.&lt;/p&gt;
&lt;p&gt;So... how about converting my blog's PostgreSQL database to SQLite, then dumping it to disk with &lt;code&gt;sqlite-diffable&lt;/code&gt; and committing the result to a git repository? And then running that in a GitHub Action?&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/main/.github/workflows/backup.yml"&gt;the workflow&lt;/a&gt;. It does exactly that, with a few extra steps: it only grabs a subset of my tables, and it redacts the &lt;code&gt;password&lt;/code&gt; column from my &lt;code&gt;auth_user&lt;/code&gt; table so that my hashed password isn't exposed in the backup.&lt;/p&gt;
&lt;p&gt;I now have &lt;a href="https://github.com/simonw/simonwillisonblog-backup/commits/main"&gt;a commit log&lt;/a&gt; of changes to my blog's database!&lt;/p&gt;
&lt;p&gt;I've set it to run nightly, but I can trigger it manually by clicking a button too.&lt;/p&gt;
&lt;h4 id="til-this-week-46"&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/readthedocs/custom-subdomain.md"&gt;Pointing a custom subdomain at Read the Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/pytest/pytest-code-coverage.md"&gt;Code coverage using pytest and codecov.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/readthedocs/readthedocs-search-api.md"&gt;Read the Docs Search API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/heroku/programatic-access-postgresql.md"&gt;Programatically accessing Heroku PostgreSQL from GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/macos/find-largest-sqlite.md"&gt;Finding the largest SQLite files on a Mac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/github-actions/grep-tests.md"&gt;Using grep to write tests in CI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="releases-this-week-46"&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/0.14"&gt;datasette-graphql 0.14&lt;/a&gt; - 2020-08-20&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/0.13"&gt;datasette-graphql 0.13&lt;/a&gt; - 2020-08-19&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-schema-versions/releases/tag/0.1"&gt;datasette-schema-versions 0.1&lt;/a&gt; - 2020-08-19&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/0.12.3"&gt;datasette-graphql 0.12.3&lt;/a&gt; - 2020-08-19&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dogsheep/github-to-sqlite/releases/tag/2.5"&gt;github-to-sqlite 2.5&lt;/a&gt; - 2020-08-18&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-publish-vercel/releases/tag/0.8"&gt;datasette-publish-vercel 0.8&lt;/a&gt; - 2020-08-17&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-cluster-map/releases/tag/0.12"&gt;datasette-cluster-map 0.12&lt;/a&gt; - 2020-08-16&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette/releases/tag/0.48"&gt;datasette 0.48&lt;/a&gt; - 2020-08-16&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/0.12.2"&gt;datasette-graphql 0.12.2&lt;/a&gt; - 2020-08-16&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-saved-queries/releases/tag/0.2.1"&gt;datasette-saved-queries 0.2.1&lt;/a&gt; - 2020-08-15&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette/releases/tag/0.47.3"&gt;datasette 0.47.3&lt;/a&gt; - 2020-08-15&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-upload-csvs/releases/tag/0.5"&gt;datasette-upload-csvs 0.5&lt;/a&gt; - 2020-08-15&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.7"&gt;asgi-csrf 0.7&lt;/a&gt; - 2020-08-15&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.7a0"&gt;asgi-csrf 0.7a0&lt;/a&gt; - 2020-08-15&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.7a0"&gt;asgi-csrf 0.7a0&lt;/a&gt; - 2020-08-15&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-cluster-map/releases/tag/0.11.1"&gt;datasette-cluster-map 0.11.1&lt;/a&gt; - 2020-08-14&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-cluster-map/releases/tag/0.11"&gt;datasette-cluster-map 0.11&lt;/a&gt; - 2020-08-14&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/0.12.1"&gt;datasette-graphql 0.12.1&lt;/a&gt; - 2020-08-13&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/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git"&gt;git&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/natalie-downe"&gt;natalie-downe&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/graphql"&gt;graphql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/inaturalist"&gt;inaturalist&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="databases"/><category term="git"/><category term="github"/><category term="natalie-downe"/><category term="projects"/><category term="graphql"/><category term="datasette"/><category term="inaturalist"/><category term="weeknotes"/></entry><entry><title>Datasette 0.46</title><link href="https://simonwillison.net/2020/Aug/9/datasette-046/#atom-tag" rel="alternate"/><published>2020-08-09T16:57:58+00:00</published><updated>2020-08-09T16:57:58+00:00</updated><id>https://simonwillison.net/2020/Aug/9/datasette-046/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datasette.readthedocs.io/en/latest/changelog.html#v0-46"&gt;Datasette 0.46&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I just released Datasette 0.46 with a security fix for an issue involving CSRF tokens on canned query pages, plus a new debugging tool, improved file downloads and a bunch of other smaller improvements.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1292501452858912769"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&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/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="projects"/><category term="security"/><category term="datasette"/></entry><entry><title>Weeknotes, I guess</title><link href="https://simonwillison.net/2020/Jun/4/weeknotes-i-guess/#atom-tag" rel="alternate"/><published>2020-06-04T23:54:04+00:00</published><updated>2020-06-04T23:54:04+00:00</updated><id>https://simonwillison.net/2020/Jun/4/weeknotes-i-guess/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://www.theguardian.com/us-news/george-floyd"&gt;What a week&lt;/a&gt;. Hard to work up the enthusiasm to write about what I’ve been working on.&lt;/p&gt;
&lt;p&gt;I’ve mainly been pushing towards shipping a Datasette release with &lt;a href="https://github.com/simonw/datasette/issues/698"&gt;writeable canned queries&lt;/a&gt;. This lead me down various other rabbit holes.&lt;/p&gt;
&lt;h3 id="authentication"&gt;Authentication&lt;/h3&gt;
&lt;p&gt;Once you can write to a database, authentication and permissions become more than just a nice-to-have. I’ve used plugins for this in the past (&lt;a href="https://github.com/simonw/datasette-auth-github"&gt;datasette-auth-github&lt;/a&gt; and &lt;a href="https://github.com/simonw/datasette-auth-existing-cookies"&gt;datasette-auth-existing-cookies&lt;/a&gt;), but to allow these plugins to work together with other features it makes sense to bring the concept of authentications and permission checks into Datasette core.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette/issues/699"&gt;Issue #699&lt;/a&gt; tracks my thinking on this. I&amp;#39;ve landed two new plugin hooks: &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html#actor-from-request-datasette-request"&gt;actor_from_request&lt;/a&gt;, which lets plugins decide if the request is from an authenticated actor (a logged-in user or an authenticated API key of some sort) and &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html#permission-allowed-datasette-actor-action-resource-type-resource-identifier"&gt;permission_allowed&lt;/a&gt; which can answer if an actor is allowed to perform a specific action on a resource.&lt;/p&gt;
&lt;h3 id="flash-messages"&gt;Flash messages&lt;/h3&gt;
&lt;p&gt;When you perform a write, I need to let you know if it worked or not. Django has &lt;a href="https://docs.djangoproject.com/en/3.0/ref/contrib/messages/"&gt;messages&lt;/a&gt;, Flask calls them &lt;a href="https://flask.palletsprojects.com/en/1.1.x/patterns/flashing/"&gt;flash messages&lt;/a&gt;. I&amp;#39;ve now &lt;a href="https://github.com/simonw/datasette/issues/790"&gt;added these to Datasette&lt;/a&gt;, using signed cookies. The new &lt;a href="https://latest.datasette.io/-/messages"&gt;/-/messages&lt;/a&gt; debug tool lets you try them out and see how they work.&lt;/p&gt;
&lt;h3 id="csrf-protection"&gt;CSRF protection&lt;/h3&gt;
&lt;p&gt;Still a &lt;a href="https://github.com/simonw/datasette/issues/793"&gt;work in progress&lt;/a&gt; (and a blocker on releasing the above new features). I shipped &lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.3"&gt;asgi-csrf 0.3&lt;/a&gt; yesterday which is now ready for use in Datasette. The next step is to integrate it.&lt;/p&gt;
&lt;h3 id="new-milestone-datasette-1-0"&gt;New milestone: Datasette 1.0&lt;/h3&gt;
&lt;p&gt;Writeable canned queries are the last major feature I want to add before Datasette 1.0. I&amp;#39;ve &lt;a href="https://github.com/simonw/datasette/issues/519"&gt;put some notes together&lt;/a&gt; on what this means: essentially I want 1.0 to signify that plugin builders and template authors can develop against Datasette with confidence that their stuff won&amp;#39;t break until at least 2.0. I also started a &lt;a href="https://github.com/simonw/datasette/milestone/7"&gt;Datasette 1.0 milestone&lt;/a&gt;.&lt;/p&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/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="datasette"/><category term="weeknotes"/></entry><entry><title>Weeknotes: datasette-ics, datasette-upload-csvs, datasette-configure-fts, asgi-csrf</title><link href="https://simonwillison.net/2020/Mar/4/weeknotes-plethora/#atom-tag" rel="alternate"/><published>2020-03-04T02:27:47+00:00</published><updated>2020-03-04T02:27:47+00:00</updated><id>https://simonwillison.net/2020/Mar/4/weeknotes-plethora/#atom-tag</id><summary type="html">
    &lt;p&gt;I've been preparing for the &lt;a href="https://www.ire.org/events-and-training/conferences/nicar-2020"&gt;NICAR 2020&lt;/a&gt; Data Journalism conference this week which has lead me into a flurry of activity across a plethora of different projects and plugins.&lt;/p&gt;

&lt;h3 id="weeknotes-24-datasette-ics"&gt;datasette-ics&lt;/h3&gt;

&lt;p&gt;NICAR publish &lt;a href="https://github.com/ireapps/nicar-2020-schedule"&gt;their schedule&lt;/a&gt; as a CSV file. I couldn't resist loading it into &lt;a href="https://nicar-2020.glitch.me/"&gt;a Datasette on Glitch&lt;/a&gt;, which inspired me to put together a plugin I've been wanting for ages: &lt;a href="https://github.com/simonw/datasette-ics"&gt;datasette-ics&lt;/a&gt;, a &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html#register-output-renderer-datasette"&gt;register_output_renderer()&lt;/a&gt; plugin that can produce a subscribable iCalendar file from an arbitrary SQL query.&lt;/p&gt;

&lt;p&gt;It's based on &lt;a href="https://github.com/simonw/datasette-atom"&gt;datasette-atom&lt;/a&gt; and works in a similar way: you construct a query that outputs a required set of columns (&lt;code&gt;event_name&lt;/code&gt; and &lt;code&gt;event_dtstart&lt;/code&gt; as a minimum), then add the &lt;code&gt;.ics&lt;/code&gt; extension to get back an iCalendar file.&lt;/p&gt;

&lt;p&gt;You can optionally also include &lt;code&gt;event_dtend&lt;/code&gt;, &lt;code&gt;event_duration&lt;/code&gt;, &lt;code&gt;event_description&lt;/code&gt;, &lt;code&gt;event_uid&lt;/code&gt; and most importantly &lt;code&gt;event_tz&lt;/code&gt;, which can contain a timezone string. Figuring out how to handle timezones was &lt;a href="https://github.com/simonw/datasette-ics/issues/1"&gt;the fiddliest part of the project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're going to NICAR, subscribe to &lt;a href="https://nicar-2020.glitch.me/data/calendar.ics"&gt;https://nicar-2020.glitch.me/data/calendar.ics&lt;/a&gt; in a calendar application to get the full 261 item schedule.&lt;/p&gt;

&lt;p&gt;If you just want to see what the iCalendar feed looks like, add &lt;code&gt;?_plain=1&lt;/code&gt; to preview it with a &lt;code&gt;text/plain&lt;/code&gt; content type: &lt;a href="https://nicar-2020.glitch.me/data/calendar.ics?_plain=1"&gt;https://nicar-2020.glitch.me/data/calendar.ics?_plain=1&lt;/a&gt; - and here's &lt;a href="https://nicar-2020.glitch.me/data/calendar"&gt;the SQL query&lt;/a&gt; that powers it.&lt;/p&gt;

&lt;h3 id="weeknotes-24-datasette-upload-csvs"&gt;datasette-upload-csvs&lt;/h3&gt;

&lt;p&gt;My work on &lt;a href="https://simonwillison.net/tags/datasettecloud/"&gt;Datasette Cloud&lt;/a&gt; is inspiring all kinds of interesting work on plugins. I released &lt;a href="https://github.com/simonw/datasette-upload-csvs"&gt;datasette-upload-csvs&lt;/a&gt; a while ago, but now that Datasette has &lt;a href="https://simonwillison.net/2020/Feb/26/weeknotes-datasette-writes/"&gt;official write support&lt;/a&gt; I've been upgrading the plugin to hopefully achieve its full potential.&lt;/p&gt;

&lt;p&gt;In particular, I've been improving its usability. CSV files can be big - and if you're uploading 100MB of CSV it's not particularly reassuring if your browser just sits for a few minutes spinning on the status bar.&lt;/p&gt;

&lt;p&gt;So I added two progress bars to the plugins. The first is a client-side progress bar that shows you the progress of the initial file upload. I used the &lt;code&gt;XMLHttpRequest&lt;/code&gt; pattern (and the drag-and-drop recipe) from Joseph Zimmerman's useful article &lt;a href="https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/"&gt;How To Make A Drag-and-Drop File Uploader With Vanilla JavaScript&lt;/a&gt; - &lt;code&gt;fetch()&lt;/code&gt; doesn't reliably report upload progres just yet.&lt;/p&gt;

&lt;p&gt;I'm using &lt;a href="https://www.starlette.io/"&gt;Starlette&lt;/a&gt; and &lt;code&gt;asyncio&lt;/code&gt; so uploading large files doesn't tie up server resources in the same way that it would if I was using processes and threads.&lt;/p&gt;

&lt;p&gt;The second progress bar relates to server-side processing of the file: churning through 100,000 rows of CSV data and inserting them into SQLite can take a while, and I wanted users to be able to see what was going on.&lt;/p&gt;

&lt;p&gt;Here's an animation screenshot of how the interface looks now:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/upload-csvs.gif" style="max-width: 100%!" alt="Uploading a CSV" /&gt;&lt;/p&gt;

&lt;p&gt;Implementing this was trickier. In the end I took advantage of the new dedicaed write thread made available by &lt;code&gt;datasette.execute_write_fn()&lt;/code&gt; - since that thread has exclusive access to write to the database, I create a SQLite table called &lt;code&gt;_csv_progress_&lt;/code&gt; and write a new record to it every 10 rows. I use the number of bytes in the CSV file as the total and track how far through that file Python's CSV parser has got using &lt;code&gt;file.tell()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It seems to work really well. The full &lt;a href="https://github.com/simonw/datasette-upload-csvs/blob/013d540797b2600bb34cfb8a923386d83f5ff25d/datasette_upload_csvs/app.py#L30-L123"&gt;server-side code is here&lt;/a&gt; - the progress bar itself then &lt;a href="https://github.com/simonw/datasette-upload-csvs/blob/013d540797b2600bb34cfb8a923386d83f5ff25d/datasette_upload_csvs/templates/upload_csv.html#L122-L145"&gt;polls Datasette's JSON API&lt;/a&gt; for the record in the &lt;code&gt;_csv_progress_&lt;/code&gt; table.&lt;/p&gt;

&lt;h3 id="weeknotes-24-datasette-configure-fts"&gt;datasette-configure-fts&lt;/h3&gt;

&lt;p&gt;SQLite ships with &lt;a href="https://www.sqlite.org/fts5.html"&gt;a decent implementation&lt;/a&gt; of full-text search. Datasette knows how to tell if a table has been configured for full-text search and adds a search box to the table page, &lt;a href="https://datasette.readthedocs.io/en/stable/full_text_search.html"&gt;documented here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-configure-fts"&gt;datasette-configure-fts&lt;/a&gt; is a new plugin that provides an interface for configuring search against existing SQLite tables. Under the hood it uses the &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/python-api.html#enabling-full-text-search"&gt;sqlite-utils full-text search methods&lt;/a&gt; to configure the table and set up triggers to keep the index updated as data in the table changes.&lt;/p&gt;

&lt;p&gt;It's pretty simple, but it means that users of Datasette Cloud can upload a potentially enormous CSV file and then click to set specific columns as searchable. It's a fun example of the kind of things that can be built with Datasette`s new write capabilities.&lt;/p&gt;

&lt;h3 id="weeknotes-24-asgi-csrf"&gt;asgi-csrf&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://simonwillison.net/tags/csrf/"&gt;CSRF&lt;/a&gt; is one of my favourite web application security vulnerabilties - I first wrote about it on this blog &lt;a href="https://simonwillison.net/2005/May/6/bad/"&gt;back in 2005&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;I was surprised to see that the Starlette/ASGI ecosystem doesn't yet have much in the way of CSRF prevention. The best option I could find to use &lt;a href="https://wtforms.readthedocs.io/en/stable/csrf.html"&gt;the WTForms library&lt;/a&gt; with Starlette.&lt;/p&gt;

&lt;p&gt;I don't need a full forms library for my purposes (at least not yet) but I needed CSRF protection for &lt;code&gt;datasete-configure-fts&lt;/code&gt;, so I've started working on a small ASGI middleware library called &lt;a href="https://github.com/simonw/asgi-csrf"&gt;asgi-csrf&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's modelled on a subset of Django's &lt;a href="https://github.com/django/django/blob/3.0.3/django/middleware/csrf.py"&gt;robust CSRF prevention&lt;/a&gt;. The README warns people NOT to trust it yet - there are still &lt;a href="https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet#double-submit-cookie"&gt;some OWASP recommendations&lt;/a&gt; that it needs to apply (&lt;a href="https://github.com/simonw/asgi-csrf/issues/2"&gt;issue here&lt;/a&gt;) and I'm not yet ready to declare it robust and secure. It's a start though, and feels like exactly the kind of problem that ASGI middleware is meant to address.&lt;/p&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/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/icalendar"&gt;icalendar&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/search"&gt;search&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="data-journalism"/><category term="icalendar"/><category term="plugins"/><category term="projects"/><category term="search"/><category term="security"/><category term="datasette"/><category term="asgi"/><category term="weeknotes"/><category term="datasette-cloud"/></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><entry><title>OWASP Top 10 2007-2017: The Fall of CSRF</title><link href="https://simonwillison.net/2018/Aug/6/owasp-csrf/#atom-tag" rel="alternate"/><published>2018-08-06T22:02:39+00:00</published><updated>2018-08-06T22:02:39+00:00</updated><id>https://simonwillison.net/2018/Aug/6/owasp-csrf/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://nvisium.com/blog/2017/11/30/owasp-top-10-2007-2017-the-fall-of-csrf.html"&gt;OWASP Top 10 2007-2017: The Fall of CSRF&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I was surprised to learn recently that CSRF didn’t make it into the 2017 OWASP Top 10 security vulnerabilities (after featuring almost every year since the list started). The credited reason is that web frameworks do a good enough job protecting against CSRF by default that it’s no longer a top-ten problem. Defaults really do matter.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/owasp"&gt;owasp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="owasp"/><category term="security"/></entry><entry><title>What are key considerations when building behind the firewall web apps?</title><link href="https://simonwillison.net/2013/Sep/15/what-are-key-considerations/#atom-tag" rel="alternate"/><published>2013-09-15T15:24:00+00:00</published><updated>2013-09-15T15:24:00+00:00</updated><id>https://simonwillison.net/2013/Sep/15/what-are-key-considerations/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/What-are-key-considerations-when-building-behind-the-firewall-web-apps/answer/Simon-Willison"&gt;What are key considerations when building behind the firewall web apps?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CSRF and XSS are still important: don't  leave any security vulnerabilities which might allow an evil website out on the internet to run JavaScript that steals data from your behind-the-firewall web application.&lt;/p&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/enterprise"&gt;enterprise&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-engineering"&gt;software-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webapps"&gt;webapps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/saas"&gt;saas&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="enterprise"/><category term="software-engineering"/><category term="webapps"/><category term="quora"/><category term="saas"/></entry><entry><title>CSRF: Flash + 307 redirect = Game Over</title><link href="https://simonwillison.net/2011/Feb/10/csrf/#atom-tag" rel="alternate"/><published>2011-02-10T22:07:00+00:00</published><updated>2011-02-10T22:07:00+00:00</updated><id>https://simonwillison.net/2011/Feb/10/csrf/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html"&gt;CSRF: Flash + 307 redirect = Game Over&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here’s the exploit that Django and Rails both just released fixes for. It’s actually a flaw in the Flash player. Flash isn’t meant to be able to make cross-domain HTTP requests with custom HTTP headers unless the crossdomain.xml file on the other domain allows them to, but it turns out a 307 redirect (like a 302, but allows POST data to be forwarded) confuses the Flash player in to not checking the crossdomain.xml on the host it is being redirect to.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/crossdomainxml"&gt;crossdomainxml&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flash"&gt;flash&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="crossdomainxml"/><category term="csrf"/><category term="django"/><category term="flash"/><category term="rails"/><category term="security"/><category term="recovered"/></entry><entry><title>Why do some people disable JavaScript in their browser?</title><link href="https://simonwillison.net/2010/Aug/25/why-do-some-people/#atom-tag" rel="alternate"/><published>2010-08-25T13:37:00+00:00</published><updated>2010-08-25T13:37:00+00:00</updated><id>https://simonwillison.net/2010/Aug/25/why-do-some-people/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/Why-do-some-people-disable-JavaScript-in-their-browser/answer/Simon-Willison"&gt;Why do some people disable JavaScript in their browser?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For security reasons.&lt;/p&gt;

&lt;p&gt;Many (most?) web applications have security vulnerabilities due to JavaScript. The most common are XSS, where malicious JavaScript can be injected in to a page, stealing cookies and forcing users to perform actions that they did not intend, and CSRF, where unprotected forms can be abused to again perform unintended actions.&lt;/p&gt;

&lt;p&gt;It's amazing how many developers are unaware of CSRF - and quite a lot still don't fully understand the consequences of XSS.&lt;/p&gt;

&lt;p&gt;Disabling JavaScript in your browser makes the above attacks much harder to pull off. Once you understand how serious and widespread these problems are, it becomes very tempting to disable JavaScript for unknown sites and only enable it for sites you think have extremely skilled development teams.&lt;/p&gt;

&lt;p&gt;That said, I don't personally disable JS in my browsers - but that's only because I haven't been bitten badly yet.&lt;/p&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/javascript"&gt;javascript&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/xss"&gt;xss&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csrf"/><category term="javascript"/><category term="privacy"/><category term="security"/><category term="xss"/><category term="quora"/></entry><entry><title>OpenCart CSRF Vulnerability</title><link href="https://simonwillison.net/2010/May/25/opencart/#atom-tag" rel="alternate"/><published>2010-05-25T00:00:00+00:00</published><updated>2010-05-25T00:00:00+00:00</updated><id>https://simonwillison.net/2010/May/25/opencart/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.visionsource.org/2010/01/28/opencart-csrf-vulnerability/"&gt;OpenCart CSRF Vulnerability&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Avoid OpenCart—it’s vulnerable to CSRF, but the maintainer has no intention of fixing it as “there is no way that I’m responsible for a client being stupid enough to click links in emails”.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/opencart"&gt;opencart&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="security"/><category term="recovered"/><category term="opencart"/></entry><entry><title>Django 1.2 release notes</title><link href="https://simonwillison.net/2010/May/17/django/#atom-tag" rel="alternate"/><published>2010-05-17T21:11:00+00:00</published><updated>2010-05-17T21:11:00+00:00</updated><id>https://simonwillison.net/2010/May/17/django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://docs.djangoproject.com/en/dev/releases/1.2/"&gt;Django 1.2 release notes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Released today, this is a terrific upgrade. Multiple database connections, model validation, improved CSRF protection, a messages framework, the new smart if template tag and lots, lots more. I’ve been using the 1.2 betas for a major new project over the past few months and it’s been smooth sailing all the way.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.djangoproject.com/weblog/2010/may/17/12/"&gt;Django 1.2 released&lt;/a&gt;&lt;/small&gt;&lt;/p&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/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/multidb"&gt;multidb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releases"&gt;releases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="django"/><category term="multidb"/><category term="open-source"/><category term="python"/><category term="releases"/><category term="recovered"/></entry><entry><title>What's new in Django 1.2 alpha 1</title><link href="https://simonwillison.net/2010/Jan/7/django/#atom-tag" rel="alternate"/><published>2010-01-07T19:31:50+00:00</published><updated>2010-01-07T19:31:50+00:00</updated><id>https://simonwillison.net/2010/Jan/7/django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://docs.djangoproject.com/en/dev/releases/1.2-alpha-1/#what-s-new-in-django-1-2-alpha-1"&gt;What&amp;#x27;s new in Django 1.2 alpha 1&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Multiple database support, improved CSRF prevention, a messages framework (similar to the Rails “flash” feature), model validation, custom e-mail backends, template caching for much faster handling of the include and extends tags, read only fields in the admin, a better if tag and more. Very exciting release.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.djangoproject.com/weblog/2010/jan/06/12-alpha-1/"&gt;Django Weblog&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/alpha"&gt;alpha&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releases"&gt;releases&lt;/a&gt;&lt;/p&gt;



</summary><category term="alpha"/><category term="csrf"/><category term="django"/><category term="django-admin"/><category term="python"/><category term="releases"/></entry><entry><title>Django ponies: Proposals for Django 1.2</title><link href="https://simonwillison.net/2009/Sep/28/ponies/#atom-tag" rel="alternate"/><published>2009-09-28T23:32:04+00:00</published><updated>2009-09-28T23:32:04+00:00</updated><id>https://simonwillison.net/2009/Sep/28/ponies/#atom-tag</id><summary type="html">
    &lt;p&gt;I've decided to step up my involvement in Django development in the run-up to Django 1.2, so I'm currently going through several years worth of accumulated pony requests figuring out which ones are worth advocating for. I'm also ensuring I have the code to back them up - my innocent &lt;a href="http://code.djangoproject.com/wiki/AutoEscaping"&gt;AutoEscaping proposal&lt;/a&gt; a few years ago resulted in an enormous amount of work by Malcolm and I don't think he'd appreciate a repeat performance.&lt;/p&gt;

&lt;p&gt;I'm not a big fan of branches when it comes to exploratory development - they're fine for doing the final implementation once an approach has been agreed, but I don't think they are a very effective way of discussing proposals. I'd much rather see working code in a separate application - that way I can try it out with an existing project without needing to switch to a new Django branch. Keeping code out of a branch also means people can start using it for real development work, making the API much easier to evaluate. Most of my proposals here have accompanying applications on GitHub.&lt;/p&gt;

&lt;p&gt;I've recently got in to the habit of including an "examples" directory with each of my experimental applications. This is a full Django project (with settings.py, urls.py and manage.py files) which serves two purposes. Firstly, it allows developers to run the application's unit tests without needing to install it in to their own pre-configured project, simply by changing in to the examples directory and running &lt;samp&gt;./manage.py test&lt;/samp&gt;. Secondly, it gives me somewhere to put demonstration code that can be viewed in a browser using the runserver command - a further way of making the code easier to evaluate. &lt;a href="http://github.com/simonw/django-safeform"&gt;django-safeform&lt;/a&gt; is a good example of this pattern.&lt;/p&gt;

&lt;p&gt;Here's my current list of ponies, in rough order of priority.&lt;/p&gt;

&lt;h4&gt;Signing and signed cookies&lt;/h4&gt;

&lt;p&gt;Signing strings to ensure they have not yet been tampered with is a crucial technique in web application security. As with all cryptography, it's also surprisingly difficult to do correctly. &lt;a href="http://vnhacker.blogspot.com/2009/09/flickrs-api-signature-forgery.html"&gt;A vulnerability in the signing implementation&lt;/a&gt; used to protect the Flickr API was revealed just today.&lt;/p&gt;

&lt;p&gt;One of the many uses of signed strings is to implement signed cookies. Signed cookies are fantastically powerful - they allow you to send cookies safe in the knowledge that your user will not be able to alter them without you knowing. This dramatically reduces the need for sessions - most web apps use sessions for security rather than for storing large amounts of data, so moving that "logged in user ID" value to a signed cookie eliminates the need for session storage entirely, saving a round-trip to persistent storage on every request.&lt;/p&gt;

&lt;p&gt;This has particularly useful implications for scaling - you can push your shared secret out to all of your front end web servers and scale horizontally, with no need for shared session storage just to handle simple authentication and "You are logged in as X" messages.&lt;/p&gt;

&lt;p&gt;The latest version of my &lt;a href="http://github.com/simonw/django-openid"&gt;django-openid&lt;/a&gt; library uses signed cookies to store the OpenID you log in with, removing the need to configure Django's session storage. I've extracted that code in to &lt;a href="http://github.com/simonw/django-signed"&gt;django-signed&lt;/a&gt;, which I hope to evolve in to something suitable for inclusion in &lt;samp&gt;django.utils&lt;/samp&gt;.&lt;/p&gt;

&lt;p&gt;Please note that django-signed has not yet been vetted by cryptography specialists, something I plan to fix before proposing it for final inclusion in core.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="http://github.com/simonw/django-signed"&gt;django-signed&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://code.djangoproject.com/wiki/Signing"&gt;Details of the Signing proposal&lt;/a&gt; on the Django wiki&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/133509246caf1d91"&gt;Signing discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Improved CSRF support&lt;/h4&gt;

&lt;p&gt;This is mainly Luke Plant's pony, but I'm very keen to see it happen. Django has shipped with CSRF protection for &lt;a href="http://code.djangoproject.com/changeset/2868"&gt;more than three years now&lt;/a&gt;, but the approach (using middleware to rewrite form HTML) is relatively crude and, crucially, the protection isn't turned on by default. Hint: if you aren't 100% positive you are protected against &lt;a href="http://en.wikipedia.org/wiki/Cross-site_request_forgery"&gt;CSRF&lt;/a&gt;, you should probably go and turn it on.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://bitbucket.org/spookylukey/django-trunk-lukeplant/src/05f0530f3207/django/contrib/csrf/"&gt;Luke's approach&lt;/a&gt; is an iterative improvement - a template tag (with a dependency on RequestContext) is used to output the hidden CSRF field, with middleware used to set the cookie and perform the extra validation. I experimented at length with an alternative solution based around extending Django's form framework to treat CSRF as just another aspect of validation - you can see the result in my &lt;a href="http://github.com/simonw/django-safeform"&gt;django-safeform&lt;/a&gt; project. My approach avoids middleware and template tags in favour of a view decorator to set the cookie and a class decorator to add a CSRF check to the form itself.&lt;/p&gt;

&lt;p&gt;While my approach works, the effort involved in upgrading existing code to it is substantial, compared to a much easier upgrade path for Luke's middleware + template tag approach. The biggest advantage of safeform is that it allows CSRF failure messages to be shown inline on the form, without losing the user's submission - the middleware check means showing errors as a full page without redisplaying the form. It looks like it should be possible to bring that aspect of safeform back to the middleware approach, and I plan to put together a patch for that over the next few days.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;Luke's &lt;a href="http://bitbucket.org/spookylukey/django-trunk-lukeplant/src/05f0530f3207/django/contrib/csrf/"&gt;CSRF branch&lt;/a&gt; on bitbucket&lt;/li&gt;
    &lt;li&gt;My &lt;a href="http://github.com/simonw/django-signed"&gt;django-safeform&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://code.djangoproject.com/wiki/CsrfProtection"&gt;Details of the CSRF proposal&lt;/a&gt; on the Django wiki&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/3d2dc750082103dc"&gt;CSRF discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Better support for outputting HTML&lt;/h4&gt;

&lt;p&gt;This is a major pet peeve of mine. Django's form framework is excellent - one of the best features of the framework. There's just one thing that bugs me about it - it outputs full form widgets (for &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt; and the like) so that it can include the previous value when redisplaying a form during validation, but it does so using XHTML syntax.&lt;/p&gt;

&lt;p&gt;I have a strong preference for an HTML 4.01 strict doctype, and all those &amp;lt;self-closing-tags /&amp;gt; have been niggling away at me for literally &lt;em&gt;years&lt;/em&gt;. Django bills itself as a framework for "perfectionists with deadlines", so I feel justified in getting wound up out of proportion over this one.&lt;/p&gt;

&lt;p&gt;A year ago I started experimenting with a solution, and came up with &lt;a href="http://github.com/simonw/django-html"&gt;django-html&lt;/a&gt;. It introduces two new Django template tags - &lt;code&gt;{% doctype %}&lt;/code&gt; and &lt;code&gt;{% field %}&lt;/code&gt;. The doctype tag serves two purposes - it outputs a particular doctype (saving you from having to remember the syntax) and it records that doctype in Django's template context object. The field tag is then used to output form fields, but crucially it gets to take the current doctype in to account.&lt;/p&gt;

&lt;p&gt;The field tag can also be used to add extra HTML attributes to form widgets from within the template itself, solving another small frustration about the existing form library. The &lt;a href="http://github.com/simonw/django-html/blob/master/README.rst"&gt;README&lt;/a&gt; describes the new tags in detail.&lt;/p&gt;

&lt;p&gt;The way the tags work is currently a bit of a hack - if merged in to Django core they could be more cleanly implemented by refactoring the form library slightly. This refactoring is currently being discussed on the mailing list.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="http://github.com/simonw/django-html"&gt;django-html&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/bbf75f0eeaf9fa64"&gt;Improved HTML discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Logging&lt;/h4&gt;

&lt;p&gt;This is the only proposal for which I don't yet have any code. I want to add official support for Python's standard logging framework to Django. It's possible to use this at the moment (I've done so on several projects) but it's not at all clear what the best way of doing so is, and Django doesn't use it internally at all. I posted a &lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/8551ecdb7412ab22"&gt;full argument in favour of logging&lt;/a&gt; to the mailing list, but my favourite argument is this one:&lt;/p&gt;

&lt;blockquote cite="http://groups.google.com/group/django-developers/browse_thread/thread/8551ecdb7412ab22"&gt;&lt;p&gt;Built-in support for logging reflects a growing reality of modern Web development: more and more sites have interfaces with external web service APIs, meaning there are plenty of things that could go wrong that are outside the control of the developer. Failing gracefully and logging what happened is the best way to deal with 3rd party problems - much better than throwing a 500 and leaving no record of what went wrong.&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;I'm not actively pursuing this one yet, but I'm very interesting in hearing people's opinions on the best way to configure and use the Python logging module in production.&lt;/p&gt;

&lt;h4&gt;A replacement for get_absolute_url()&lt;/h4&gt;

&lt;p&gt;Django has a loose convention of encouraging people to add a &lt;code&gt;get_absolute_url&lt;/code&gt; method to their models that returns that object's URL. It's a controversial feature - for one thing, it's a bit of a layering violation since URL logic is meant to live in the &lt;samp&gt;urls.py&lt;/samp&gt; file. It's incredibly convenient though, and since it's good web citizenship for everything to have one and only one URL I think there's a pretty good argument for keeping it.&lt;/p&gt;

&lt;p&gt;The problem is, the name sucks. I first took a look at this in the last few weeks before the release of Django 1.0 - what started as a quick proposal to come up with a better name before we were stuck with it quickly descended in to a quagmire as I realised quite how broken &lt;code&gt;get_absolute_url()&lt;/code&gt; is. The short version: in some cases it means "get a relative URL starting with /", in other cases it means "get a full URL starting with http://" and the name doesn't accurately describe either.&lt;/p&gt;

&lt;p&gt;A full write-up of my investigation is &lt;a href="http://code.djangoproject.com/wiki/ReplacingGetAbsoluteUrl"&gt;available on the Wiki&lt;/a&gt;. My proposed solution was to replace it with two complementary methods - &lt;code&gt;get_url()&lt;/code&gt; and &lt;code&gt;get_url_path()&lt;/code&gt; - with the user implementing one hence allowing the other one to be automatically derived. My &lt;a href="http://github.com/simonw/django-urls"&gt;django-urls&lt;/a&gt; project illustrates the concept via a model mixin class. A year on I still think it's quite a neat idea, though as far as I can tell no one has ever actually used it.&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;&lt;a href="http://code.djangoproject.com/wiki/ReplacingGetAbsoluteUrl"&gt;ReplacingGetAbsoluteUrl&lt;/a&gt; on the wiki&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://github.com/simonw/django-urls"&gt;django-urls&lt;/a&gt; on GitHub&lt;/li&gt;
    &lt;li&gt;&lt;a href="http://groups.google.com/group/django-developers/browse_thread/thread/7e69c39c23ec1079"&gt;Recent get_absolute_url discussion&lt;/a&gt; on the django-developers mailing list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Comments on this post are open, but if you have anything to say about any of the individual proposals it would be much more useful if you posted it to the relevant mailing list thread.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cookies"&gt;cookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cryptography"&gt;cryptography&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/luke-plant"&gt;luke-plant&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markup"&gt;markup&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ponies"&gt;ponies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/signedcookies"&gt;signedcookies&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/signing"&gt;signing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xhtml"&gt;xhtml&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cookies"/><category term="cryptography"/><category term="csrf"/><category term="django"/><category term="html"/><category term="logging"/><category term="luke-plant"/><category term="markup"/><category term="ponies"/><category term="projects"/><category term="python"/><category term="security"/><category term="signedcookies"/><category term="signing"/><category term="xhtml"/></entry><entry><title>Amazon Says Listing Problem Was an Error, Not a Hack</title><link href="https://simonwillison.net/2009/Apr/14/amazon/#atom-tag" rel="alternate"/><published>2009-04-14T08:32:40+00:00</published><updated>2009-04-14T08:32:40+00:00</updated><id>https://simonwillison.net/2009/Apr/14/amazon/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.pcworld.com/businesscenter/article/163042/amazon_says_listing_problem_was_an_error_not_a_hack.html"&gt;Amazon Says Listing Problem Was an Error, Not a Hack&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“A friend within the company told him that someone working on Amazon’s French site mistagged a number of keyword categories, including the ’Gay and Lesbian’ category, as pornographic, using what’s known internally as the Browse Nodes tool. Soon the mistake affected Amazon sites worldwide.”

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://simonwillison.net/2009/Apr/13/amazonfail/#c44124"&gt;Chris Shiflett&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/amazon"&gt;amazon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/amazonfail"&gt;amazonfail&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="amazon"/><category term="amazonfail"/><category term="csrf"/><category term="security"/></entry><entry><title>How to cause moral outrage from the entire Internet in ten lines of code</title><link href="https://simonwillison.net/2009/Apr/13/amazonfail/#atom-tag" rel="alternate"/><published>2009-04-13T19:48:53+00:00</published><updated>2009-04-13T19:48:53+00:00</updated><id>https://simonwillison.net/2009/Apr/13/amazonfail/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://community.livejournal.com/brutal_honesty/3168992.html"&gt;How to cause moral outrage from the entire Internet in ten lines of code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Looks legit—the author claims to have sparked this weekend’s #amazonfail moral outrage (where Amazon where accused of removing Gay and Lesbian books from their best seller rankings) by exploiting a CSRF hole in Amazon’s “report as inappropriate” feature to trigger automatic takedowns. EDIT: His claim is disputed elsewhere (see comments)


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/amazon"&gt;amazon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/amazonfail"&gt;amazonfail&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prdisaster"&gt;prdisaster&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="amazon"/><category term="amazonfail"/><category term="csrf"/><category term="prdisaster"/><category term="security"/></entry><entry><title>17-year-old claims responsibility for Twitter worm</title><link href="https://simonwillison.net/2009/Apr/12/yearold/#atom-tag" rel="alternate"/><published>2009-04-12T19:22:19+00:00</published><updated>2009-04-12T19:22:19+00:00</updated><id>https://simonwillison.net/2009/Apr/12/yearold/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.bnonews.com/news/242.html"&gt;17-year-old claims responsibility for Twitter worm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It was a text book XSS attack—the URL on the user profile wasn’t properly escaped, allowing an attacker to insert a script element linking out to externally hosted JavaScript which then used Ajax to steal any logged-in user’s anti-CSRF token and use it to self-replicate in to their profile.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/worms"&gt;worms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xss"&gt;xss&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="security"/><category term="twitter"/><category term="worms"/><category term="xss"/></entry><entry><title>Quoting Roy Fielding</title><link href="https://simonwillison.net/2009/Jan/23/csrf/#atom-tag" rel="alternate"/><published>2009-01-23T08:14:33+00:00</published><updated>2009-01-23T08:14:33+00:00</updated><id>https://simonwillison.net/2009/Jan/23/csrf/#atom-tag</id><summary type="html">
    &lt;blockquote cite="http://lists.w3.org/Archives/Public/ietf-http-wg/2009JanMar/0037.html"&gt;&lt;p&gt;CSRF is not a security issue for the Web.  A well-designed Web service should be capable of receiving requests directed by any host, by design, with appropriate authentication where needed.  If browsers create a security issue because they allow scripts to automatically direct requests with stored security credentials onto third-party sites, without any user intervention/configuration, then the obvious fix is within the browser.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="http://lists.w3.org/Archives/Public/ietf-http-wg/2009JanMar/0037.html"&gt;Roy Fielding&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/credentials"&gt;credentials&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/royfielding"&gt;royfielding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="browsers"/><category term="credentials"/><category term="csrf"/><category term="royfielding"/><category term="security"/></entry><entry><title>Quoting Jeremiah Grossman</title><link href="https://simonwillison.net/2008/Nov/3/jeremiah/#atom-tag" rel="alternate"/><published>2008-11-03T12:43:34+00:00</published><updated>2008-11-03T12:43:34+00:00</updated><id>https://simonwillison.net/2008/Nov/3/jeremiah/#atom-tag</id><summary type="html">
    &lt;blockquote cite="http://jeremiahgrossman.blogspot.com/2008/11/browser-security-bolt-it-on-then-build.html"&gt;&lt;p&gt;When visiting any Web page, the site owner is easily able to ascertain what websites you've visited (CSS color hacks) or places you're logged-in (JavaScript errors / IMG loading behavior). They can also automatically exploit your online bank, social network, and webmail accounts (XSS). Additionally, the browser could be instructed to hack devices on the intranet, including DSL routers and printers. And, if that's not enough, they could turn you into a felon by forcing requests to illegal content or hack other sites (CSRF).&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="http://jeremiahgrossman.blogspot.com/2008/11/browser-security-bolt-it-on-then-build.html"&gt;Jeremiah Grossman&lt;/a&gt;&lt;/p&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/jeremiah-grossman"&gt;jeremiah-grossman&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xss"&gt;xss&lt;/a&gt;&lt;/p&gt;



</summary><category term="csrf"/><category term="jeremiah-grossman"/><category term="security"/><category term="xss"/></entry><entry><title>Web Security Horror Stories: The Director's Cut</title><link href="https://simonwillison.net/2008/Oct/26/horror/#atom-tag" rel="alternate"/><published>2008-10-26T12:15:33+00:00</published><updated>2008-10-26T12:15:33+00:00</updated><id>https://simonwillison.net/2008/Oct/26/horror/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://simonwillison.net/2008/talks/head-horror/"&gt;Web Security Horror Stories: The Director&amp;#x27;s Cut&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Slides from the talk on web application security I gave this morning at &amp;lt;head&amp;gt;, the worldwide online conference. I just about managed to resist the temptation to present in my boxers. Topics include XSS, CSRF, Login CSRF and Clickjacking.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/clickjacking"&gt;clickjacking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logincsrf"&gt;logincsrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xss"&gt;xss&lt;/a&gt;&lt;/p&gt;



</summary><category term="clickjacking"/><category term="csrf"/><category term="logincsrf"/><category term="security"/><category term="xss"/></entry><entry><title>Quoting Bill Zeller</title><link href="https://simonwillison.net/2008/Sep/29/popular/#atom-tag" rel="alternate"/><published>2008-09-29T13:11:23+00:00</published><updated>2008-09-29T13:11:23+00:00</updated><id>https://simonwillison.net/2008/Sep/29/popular/#atom-tag</id><summary type="html">
    &lt;blockquote cite="http://www.freedom-to-tinker.com/blog/wzeller/popular-websites-vulnerable-cross-site-request-forgery-attacks"&gt;&lt;p&gt;We've found CSRF vulnerabilities in sites that have a huge incentive to do security correctly. If you're in charge of a website and haven't specifically protected against CSRF, chances are you're vulnerable.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="http://www.freedom-to-tinker.com/blog/wzeller/popular-websites-vulnerable-cross-site-request-forgery-attacks"&gt;Bill Zeller&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/bill-zeller"&gt;bill-zeller&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;&lt;/p&gt;



</summary><category term="bill-zeller"/><category term="csrf"/><category term="security"/></entry></feed>