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

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

Compulsive curiosity, or, how I built an infinite idea machine Gift details on the subscriber portal Portal link in the archive nav First, add no friction: How micropayments lost and subscriptions won Filter subscribers and automations by source Automations, rebuilt What email will look like in the future Filter subscribers by bounce date and reason Email could have been X.400 times better Three features are moving behind the paywall Firewall changes and improvements Put your name and voice into your company newsletter Simplified email address settings Subscription wall Inboxes were overwhelming before we'd even named them The US government tried really hard to screw up email Public postmortem: database connection exhaustion Ask a nerd: what is the best way to unsubscribe from newsletters? Bookshop.org embeds Email was into agents before they were cool Passwordless login Rename metadata keys in bulk A spring cleaning for our legal docs Ask a nerd: what happens when you click the spam button? Passkey support for two-factor authentication How Buttondown's API versioning works Safer defaults for the email creation API How to send email to space Recovery codes for two-factor authentication Filter sent emails by engagement rate How we migrated to TypeIDs without breaking clients How we check every link in your email Use newsletter metadata in your emails Should we bring back email exploders? Sort and filter by open and click rates Custom click tracking domains More newsletter settings in the API Revamped replies Custom email templates for everyone Simplified cancellation Ask a Nerd: Does email length affect deliverability? The changelog, reborn Swedish localization Forwarding an email is not always straightforward Public descriptions for tags OpenAPI spec for archives How Rodrigo brings a humanistic view to consumer technology Subscribers can come from anywhere. Even another newsletter platform's form. Survey responses on the web How Brandon Lucas Green shares his music and supports artists Your newsletter's archives are more valuable than your list Better tag self-management Smarter automation filters Granular API keys New design settings pages Snippets Ask A Nerd: How does newsletter cadence affect deliverability? Starred views More ways to customize your archives Inbox filtering Mastodon follower analytics Ask a Nerd: What are good open, click, and response rates for an email newsletter? How we migrated our database to PlanetScale Two new archive themes Custom buttons now work in Markdown mode Ask a Nerd: Does attaching files to your newsletter hurt deliverability? Seline and Tinylytics support Unban subscribers Announcement bars for your archives Bang paths, source routing, and how email trips were planned Public postmortem: archive downtime 2025 disposables.app Russian localization Ask a Nerd: Can you improve email deliverability with a personal domain? More locale options How we interview customers at Buttondown Bluesky analytics Reply to conversations Minimum viable complexity How Jeffery Hicks goes behind-the-scenes in his newsletter Changes to our stack in 2025 2026: Emails TK reminders in the editor What the hell is a UTM? Randomize survey answer order Why we insourced analytics Scroll sync in the editor 2026: Archives How Jamie Thingelstad uses Buttondown to explore tech topics How Kelly Jensen uses Buttondown to discuss key library issues Keeping feature creep at bay Improved filters Content Security Policy in archives Open source Sniperl.ink Auto-activating RSS reader subscriptions What the hell is ActivityPub? Gift subscriptions How Igor Ranc built Berlin's largest expat tech newsletter Template change history
How we enabled Content Security Policy for everyone
Matias Artopoulos Kozak · 2026-03-06 · via

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!