I spent most of yesterday making the site safer, and by the end of it I had made every image disappear. Not with an error. Not with a crash. Just blank space where my logo, my face, and my whole /link page used to be.
The day started with a security audit. I went through the usual suspects -- clickjacking, MIME sniffing, referrer leaks, the SSRF guard, the contact form getting hammered. Real fixes, the kind you put off for months because nothing is on fire. I added proper security headers in next.config.ts, including a Content-Security-Policy that tells the browser exactly which origins are allowed to load things. Razorpay for checkout, Supabase for data and storage, YouTube and OpenStreetMap for the embeds. Tight, deliberate, audited.
I checked it the way you are supposed to. Headers served correctly. No CSP violations screaming in the console on the pages I opened. tsc clean, 382 tests green. I merged it and moved on to the next thing, feeling responsible.
The part I did not check
A CSP has a separate rule for images -- img-src. I had listed Supabase storage and OpenStreetMap there, because those were the origins I remembered. What I forgot is that almost none of my actual images live on Supabase. They sit on two CDNs I set up ages ago and stopped thinking about: company-assets.bookasloth.in and website-assets.shubhamdatarkar.com. Logos, the profile photo, every tile on the /link page. All served from hosts my brand-new policy did not know about.
So the browser did exactly what I told it to. It blocked them. Silently. A CSP image block does not throw a red error in your face the way a broken script does -- it just refuses the request and leaves a gap. If you do not scroll to the pages that lean on images, you never notice.
Verified served plus no CSP refusals is not the same as verified the site still works.
— a note to my future self
The fix was three lines -- add both CDN hosts to img-src. The lesson was bigger. My verification checked that the header existed and that the pages I happened to open were clean. It never checked the thing that actually matters to a visitor: does an image render, is naturalWidth greater than zero. Those are different questions, and I had answered the easy one.
What I am taking from it
Now when I touch the CSP, I have one rule written down: both asset CDNs stay in img-src, always. And when I verify a change, I try to check the outcome a real person would see, not just the mechanism I changed. It is slower. It is also the whole point.
Security work has this quiet trap. The change looks correct, the tests pass, the headers are perfect -- and you have locked yourself out of your own house. Yesterday it was images. I would rather it stay that cheap.

