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

推荐订阅源

J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
B
Blog RSS Feed
C
Check Point Blog
D
Docker
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
MongoDB | Blog
MongoDB | Blog
博客园_首页
Apple Machine Learning Research
Apple Machine Learning Research
量子位
有赞技术团队
有赞技术团队
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
D
DataBreaches.Net
M
MIT News - Artificial intelligence
B
Blog
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
腾讯CDC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
月光博客
月光博客

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Three Cloudflare Patterns Earned the Hard Way
David Bartal · 2026-05-28 · via DEV Community

Every Cloudflare product is well-documented in isolation. The interesting bugs are always at the seams between two products — the edge injecting scripts into HTML that already has a CSP, the WAF inspecting requests for media served from R2, Vite substituting variables at build time that don't exist yet on Pages. These three patterns are from running nadiapoe.co.uk on Astro 6 + Pages, with R2 hosting the artwork media.

1. CSP nonces via middleware, not build-time hashes

The textbook way to do a strict CSP on a static site is build-time hashing: scan every inline <script>, hash it, list the hashes in the CSP header. Cloudflare even has a build hook to do it for you.

For an art site, Bot Fight Mode is non-negotiable — automated scrapers harvesting painting images are a real concern, and Cloudflare's challenge platform is the cheapest layer of protection available. But it breaks the moment you enable it. Cloudflare's edge injects the challenge widget at request time with a rotating token. The hash changes per request. Your CSP rejects it. Half your visitors get a broken challenge widget because the static hash you baked in this morning no longer matches.

The fix is a per-request nonce. Astro 6's middleware runs as a Worker; Workers have HTMLRewriter. From client/src/middleware.ts:

export const onRequest = defineMiddleware(async (context, next) => {
  const response = await next()
  if (typeof HTMLRewriter === 'undefined') return response
  if (!response.headers.get('content-type')?.includes('text/html')) return response

  const nonceBytes = crypto.getRandomValues(new Uint8Array(16))
  const nonce = btoa(String.fromCharCode(...nonceBytes))

  const rewritten = new HTMLRewriter()
    .on('script', { element(el) { el.setAttribute('nonce', nonce) } })
    .transform(response)

  const headers = new Headers(rewritten.headers)
  headers.set('Content-Security-Policy', buildCsp(nonce))
  // private cache — never share a nonce between users
  headers.set('Cache-Control', 'private, max-age=1500, stale-while-revalidate=7200')

  return new Response(rewritten.body, { status: rewritten.status, headers })
})

Enter fullscreen mode Exit fullscreen mode

Two non-obvious bits:

  • Cloudflare reads the nonce from your CSP header and stamps its own injected scripts with it. Undocumented but stable. This is the only reason the pattern works at all — without it, the challenge widget would still get blocked.
  • Cache-Control: private is load-bearing. A shared cache that served one user's nonced HTML to another would only break the cached client (their nonce doesn't match the newly injected scripts), but it's still a bug. private keeps Cloudflare's edge from doing this.

2. Server secrets via cloudflare:workers, not import.meta.env

Astro has two environments: build-time and runtime. Vite resolves import.meta.env.SOMETHING at build time by string substitution. If the value isn't in .env at build time — and Cloudflare Pages doesn't expose dashboard-configured secrets during the build — Vite bakes in the literal undefined.

Worse: it does this silently. No warning, no error. The contact form deploys, the Resend SDK call fails with "API key undefined," and you spend an hour checking the Cloudflare dashboard before realising the value never made it into the bundle.

Astro 6 has two ways to read server-side env at runtime. They are not equivalent:

// ❌ Broken in Astro 6 — Astro.locals.runtime.env was removed
const apiKey = Astro.locals.runtime.env.RESEND_API_KEY

// ✅ Works
import { env } from 'cloudflare:workers'
const apiKey = env.RESEND_API_KEY

Enter fullscreen mode Exit fullscreen mode

The cloudflare:workers module is provided by the Workers runtime. env is the live binding object — the same one your wrangler.toml and Cloudflare dashboard configure. No build-time substitution; nothing is baked in.

Client-side code (anything in a Svelte component or a non-SSR Astro page) keeps using import.meta.env.PUBLIC_*. Those are baked at build time on purpose — they're public.

One gotcha: bindings may be absent in local dev without a full Pages emulation setup. Wrap access in try/catch when the value is optional:

function getDb() {
  try { return (env as any)?.DB ?? null } catch { return null }
}

Enter fullscreen mode Exit fullscreen mode

3. R2 hotlink protection with an inverted WAF rule

Print fulfillment runs through Prodigi — a print-on-demand provider that produces and ships prints worldwide. When an order comes in, Medusa generates a time-gated presigned R2 URL pointing to the high-resolution print master and hands it to Prodigi. Prodigi fetches the file, prints it, ships it. The URL expires. Nobody else ever needs access to that file.

The high-res masters live in R2. Keeping them protected means ensuring those presigned URLs can only be opened by the intended recipient in the intended window — not scraped, not re-shared, not embedded on another site. The WAF rule is the layer that enforces origin intent on the media domain.

The naive WAF rule is "block if the Referer header doesn't match nadiapoe.co.uk."

It works until you try to:

  • Open an image URL directly in a browser tab — no Referer, blocked.
  • Load a <video preload="metadata"> tag — some browsers send no Referer on media requests, blocked.
  • Test on a *.pages.dev preview deployment — Referer is <branch>.nadiapoe-co-uk.pages.dev, blocked.

The fix is to invert the logic: block only when a Referer exists and isn't in the allowlist.

(http.host eq "media.nadiapoe.co.uk")
and (len(http.referer) > 0)
and not (http.referer contains "nadiapoe.co.uk")
and not (http.referer contains "localhost")
and not (http.referer contains ".pages.dev")

Enter fullscreen mode Exit fullscreen mode

Empty Referers pass — direct nav, preload requests, RSS readers. Preview deployments pass via the .pages.dev wildcard. Real hotlinks from other sites get a 403.

If you script your WAF rules via the Cloudflare API (which you should, for reproducibility), two quirks not mentioned in the error messages: a PUT to the rulesets endpoint must not include kind or phase (they're implicit from the URL), and rate-limit characteristics on the free plan must include cf.colo.id because counts are per-colo, not global.

Closing

The seams are where you learn things. Check the network tab when behaviour is wrong — Cloudflare adds its own response headers that tell you which rule fired, which challenge ran, which WAF block triggered. The answers are usually there; they're just not in the docs.

nadiapoe.co.uk runs on all three patterns in production.