惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Forbes - Security
Forbes - Security
T
Troy Hunt's Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Hacker News: Front Page
TaoSecurity Blog
TaoSecurity Blog
PCI Perspectives
PCI Perspectives
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Vulnerabilities – Threatpost
博客园 - 【当耐特】
V
Visual Studio Blog
N
News and Events Feed by Topic
Security Archives - TechRepublic
Security Archives - TechRepublic
V
V2EX
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
WordPress大学
WordPress大学
T
Tor Project blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
P
Proofpoint News Feed
A
Arctic Wolf
C
CERT Recently Published Vulnerability Notes
Last Week in AI
Last Week in AI
T
Tenable Blog
K
Kaspersky official blog
爱范儿
爱范儿
S
Security @ Cisco Blogs
Spread Privacy
Spread Privacy
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
博客园 - 三生石上(FineUI控件)
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
W
WeLiveSecurity
L
LINUX DO - 最新话题
Hacker News: Ask HN
Hacker News: Ask HN
阮一峰的网络日志
阮一峰的网络日志
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
小众软件
小众软件
Webroot Blog
Webroot Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Attack and Defense Labs
Attack and Defense Labs

Buttondown's blog

Email could have been X.400 times better The physicists who convinced Fermilab to send Brazil's emails Better in-app previews Analytics 3.0 Subscriber ID variables Comments! Send latest premium action Automation filtering Free API subscribers Surveys in automations Reply to replies Labels for RSS feeds How Jeremy Singer-Vine curates curious datasets for readers 2023 (and what's next) Email vs web content Sort by engagement Better gift subscriptions How Andy Dehnart built a career reviewing television New email template Email-based automations Opt-in reply tracking Automatic alt text More social network integrations Sort by metadata Overlarge image warnings Automation tag actions Pause emails mid-flight Search tags and automations Gift via automations Subscriber-driving emails Programmatic webhooks Email page views Tag statistics Discord webhook formatting Automatic subscriber cleanup RSS subscriber count Weekly subscriber reports More list columns Customizable list views How Max Voltar turned a side gig into a trusted keyboard resource How Nick Disabato runs two newsletters from one design consultancy Made-for-you share images Automation improvements End-of-email surveys Filter by date Survey-triggered automations More automation functionality New webhooks How France Insider built a news service with paid subscribers Email as primary key How John Willshire unites two businesses in one newsletter Confirmation reminders Email churned subscribers Email-to-draft Subscriber metadata columns ChatGPT integration Faster web archives Referral program Better search results TikTok embeds Subscriber timeline Spotify embeds Improved RSS-to-email Subscribe page OG image New analytics page Google Tag Manager Even more subscriber types Integrating Duda with Buttondown Linktree integration guide Advanced and enterprise plans Framer integration guide API requests page Team collaboration In-email surveys Better CSS settings Better RSS automation fetching! Editor toolbar improvements Smart filters Faster emails page RSS automations Faster email analytics Zapier error codes Image accessibility checks Tags vs newsletters OG image picker Image editor improvements API bulk actions Improved OpenAPI spec Mastodon support Better subscriber filtering Better subscriber validation Hotkey support! Programmatic access to analytics Stronger bulk actions Faster archive page Custom canonical URLs Email slug and metadata Improved writing interface Generating a Typescript router in Django Filter emails by source
How we enabled Content Security Policy for everyone
Matias Artopoulos Kozak · 2026-03-06 · via Buttondown's blog

Buttondown allows increasingly more options for authors to customize their newsletter archives. We've recently shipped better theme settings and customization options so authors can have their newsletter look like them with little effort. We also allow folks to write any kind of HTML in their emails and web archives, even including a Naked mode that lets you import fully rendered HTML from elsewhere, without our templates.

However, much of this customization requires putting a lot of care into how we render this HTML and CSS, making sure that it cannot be used for evil purposes such as phishing and spam, or even taking over other authors' accounts. The main concern here is JavaScript: because both the web archives and the actual author-facing application are served in buttondown.com, having malicious code running in a web archive could mean taking over another authors account by using the same credentials they use for authenticating into the Buttondown app.

Until a few months ago, we relied on semi-manual HTML sanitization on user-provided fields. This means calling backend libraries like nh3 in every place a user-provided string is rendered. These libraries go through all the HTML code, filtering out every inappropiate HTML tag or attribute that could cause code execution. Using these libraries can be error-prone, as accidentally using an author-provided string without sanitizing it opens the door to any kind of HTML — including <script> tags — to be included.

Fortunately, browsers have provided a great tool to filter the content (and specifically scripts) that are allowed to be loaded at all in any page: Content-Security-Policy.

Significantly improve the security of your website with this one weird trick

Content Security Policy (CSP) is a security feature available in every single modern web browser that allows web developers like us to specify exactly which JavaScript scripts and CSS stylesheets to load in any page. Basically, it lets us set up policies such as "only allow loading scripts from buttondown.com and sniperl.ink".

What's great about this feature is that it acts as a stop-gap for any potential misses in our HTML sanitization. If a <script> tag does end up showing up in the page, CSP stops it from loading if it's not in the allowlist.

How to not break stuff

The problem then is how to enable it. As with any allowlist-based system, "just toggling it on" isn't an option for a production application, as one mistake could cause users' web archives to break entirely for all users. Fortunately, CSP also includes a way to get reports when something that violates the policy attempts to load, without actually blocking it: Content-Security-Policy-Report-Only.

It works like this: first, you write down your desired CSP policy, specifying the origins you're OK with loading scripts, spreadsheets, images and others from: script-src 'self' 'https://sniperl.ink' 'https://static-assets.buttondown.com', [..].

Then, you need a "report URI" that the browser will send reports to when it detects something violates the policy. Luckily for us, we already use Sentry, which has Security Policy Reporting monitoring built-in: report-uri https://o97520.ingest.us.sentry.io/api/6063581[..].

Finally, you set this entire policy as the Content-Security-Policy-Report-Only HTTP header, where it can't be further modified even by rouge HTML code. When the page loads, the browser sees this header and uses it as the only policy for the entire website.

Because we set it as Report-Only, policy violations are only reported but not blocked. When you get the reports in Sentry, you can figure out why it happened and either avoid the script from loading or add it to the allowlist.

We initially set this up hoping that we would get one or two reports over the course of a few days and we could just fix them and finish the implementation. Unfortunately, we stumbled upon dozens of reports, where many of them were false positives from users' web extensions injecting script tags into the page.

Bad error messages

Some of the reports we got were straight up confusing. For example, we started seeing a lot of Blocked 'script' from 'sniperl.ink' (Sniper Link is our free service for showing email activation links to users.) However, sniperl.ink was explicitly included in the scripts' allowed origins in our policy, so this made no sense. Apparently, when the remote server returns an error response (like 5xx or 4xx) when trying to fetch the script, CSP interprets it as a policy block with this unhelpful message. This block was caused by Vercel's anti-bot protection probably misbehaving.

We also had to revamp the way we did our content embeds, as they sometimes conflicted with the way our CSP was designed. However, after a few back-and-fourths patching real policy bugs and investigating false-positives, we reached a point where every report over the course of a week had been accounted for, and we were ready to ship the security policy to production.

Uneventfully pulling the lever

When everything was done, we just had one thing to do: remove the -Report-Only part of the HTTP header, so the policy was actually enforced.

A screenshot of GitHub diff for the commit that actually enabled the CSP policy. It shows that we literally just removed the -Report-Only part of the HTTP header name for the CSP middleware

And then... nothing. The policy has been enforced for a few months now, and the only issues we've had were few hiccups when introducing new content embeds, but it was pretty straightfoward to fix. Now authors are better protected against targeted attacks.

I wanted to write this blog post for a while as for whatever reason, I couldn't find many "we took CSP to prod" posts that I could learn from. Hopefully this can help someone get CSP to production safely!