There's an exercise every developer should run on their own project at least once: attack it like you hate it.
I build NitroIDE — a browser-based IDE where your code never leaves your machine. No servers, no accounts, no containers. Everything runs client-side: the editor, the live preview, the console, the state watcher. That architecture is the whole point of the product. It's also, I realized this week, a very specific kind of target.
So I sat down and audited my own tool the way an attacker would. I found two critical vulnerabilities, one cache design flaw, and zero security headers. Version 25 is the release where all of that got fixed. Here's the full story, including the parts that were embarrassing.
Critical #1: the preview iframe could read your saved projects
NitroIDE lets you share code with a link — nitroide.com/tools/codebox.html?code=... — and anyone who opens it gets a running app instantly, no signup. That's the flagship feature. It's also the attack surface.
The live preview runs inside a sandboxed iframe, which sounds safe. But the sandbox attribute included allow-same-origin. Think about what that means: JavaScript inside the preview ran with nitroide.com origin privileges. A malicious share link could craft preview code that reaches out of the iframe and reads the victim's localStorage — where NitroIDE stores saved projects. Your private code, exfiltrated through a link that looked like a harmless demo.
The fix was a one-line removal with outsized consequences: drop allow-same-origin from the sandbox. The preview doesn't need it — it communicates with the parent through srcdoc and postMessage, both of which work fine on an opaque origin. One honest tradeoff I'll name plainly: user code in the preview that touches localStorage will now throw in Safari. That's the correct posture — CodePen and CodeSandbox behave the same way — and I'd rather break a convenience than leave a theft vector open.
Critical #2: the parent page trusted the iframe's messages
This one was worse, because it was my own code being naive. The parent page listens for postMessage events from the preview iframe — console logs, errors, state-watcher updates — and renders them in the IDE's console panel. The old code interpolated those messages straight into innerHTML, unescaped. Tab names and line numbers went into an onclick string.
So a malicious ?code= link didn't even need the localStorage trick. It could send a crafted message from the "preview" and get arbitrary HTML/JS executed in the parent page — full parent-origin XSS, delivered through a share link.
The fix: a sanitizeConsoleHTML() function that allowlists only the exact span/pre vocabulary the console serializer legitimately produces, plus strict validation on everything else — text escaping on state names, line numbers coerced to actual numbers, tab identifiers validated against a strict pattern, and JSON.parse wrapped in try/catch. I verified it with Node before shipping: legitimate console formatting (colors, object expansion) renders exactly as before, and four distinct attack payloads get neutralized.
The quieter fixes that still mattered
Service worker cache bloat. The service worker was runtime-caching every ?code= URL as a separate entry. Every share link anyone opened added a new cache entry. Document navigations are now cached under the bare path. Small fix, real hygiene.
Zero security headers. I'll be blunt: the site had none. No HSTS, no X-Content-Type-Options, nothing. GitHub Pages can't set headers, so I did it at the Cloudflare layer with a Transform Rule: HSTS (one year, includeSubDomains), nosniff, a strict referrer policy, and a permissions policy that disables camera, microphone, and geolocation outright. Verified live with an external header check.
The mobile scroll-lock saga. On iOS, touching the codebox made the entire screen shift and bounce — keyboard open or not. My first fix locked body with position: fixed. It did nothing. The actual root cause: iOS Safari rubber-bands the html element, not the body. The real fix locks both, scoped so desktop and other pages are untouched. Internal panes (editor, sidebar, preview) keep their own scrolling. If you've ever fought iOS scroll physics, you know this specific pain.
A contact page and honest ad policy. The site is preparing for AdSense review, which forced some honesty work: a real contact page with a working form, and the legal page rewritten to disclose ad cookies plainly instead of claiming "no tracking" while applying to run ads. If you run a site planning to monetize, do the disclosure before the review, not after.
Plus the unglamorous batch: the RSS feed had a missing <item> tag that silently killed the entire blog feed — fixed and now serving 27 valid items. Sitemap lastmod dates refreshed from git history across all 70 URLs. Changelog trimmed from 114 entries to 26 readable ones. Meta descriptions tightened, font weights fixed (the site was shipping byte-identical font files labeled as different weights — yes, really), and the PWA icon replaced with a real 512×512 asset.
What I deliberately did not do
No CSP header yet — I want to test it against every embed and template before enforcing. No claims I can't back: you'll notice I don't say "unhackable" anywhere in this post. Security is a process, and v25 is one honest step in it.
Try it
NitroIDE is free, no signup, runs entirely in your browser: https://nitroide.com
The full v25 changelog is at https://nitroide.com/changelog.html, and if you want these updates in your inbox, there's a newsletter signup in the blog footer. I read every reply.
Top comments (0)