← Writing

A static site still has an attack surface

This site is static. Astro compiles it to plain HTML and CSS: no server, no database, no login, and no session worth stealing. The comfortable conclusion is that there is nothing to attack. That conclusion held right up until I added interactive demos.

A few of the project pages carry small vanilla JavaScript islands. One of them, the ChartCraft demo, takes pasted CSV-like text and renders a live bar chart. During a security pass this week I found that each bar’s label was going into the DOM through innerHTML, straight from the pasted text. Paste a row like this and the markup executes:

<img src=x onerror=alert(1)>,5

Calling the severity honestly

This is self-XSS. The only person who can trigger it is the person doing the pasting, in their own browser, on a site that holds no credentials and sets no cookies of value. Nobody’s account gets taken over. If I were triaging someone else’s bug report against this site, I would mark it low severity and mean it.

I fixed it the same day anyway. Three reasons:

  1. Paste is a social-engineering shape. “Copy this text and paste it into the box” is exactly how support-scam and self-XSS attacks are delivered in the wild. A demo that invites pasting should not also execute what gets pasted.
  2. Demo code travels. Snippets get lifted into other projects, and the next project might have a session behind it. The habit of escaping at the boundary is worth more than the individual fix.
  3. It was cheap. The fix replaced innerHTML with createElement and textContent, with bar size and color set through the style API. A few lines, no behavior change for honest input.

What else the pass turned up

The same review found a quieter bug in another demo: a cleanup listener registered on document for Astro’s client-side router events, once per page visit, never removed. Navigate back and forth enough and the handlers pile up. Registering with { once: true } lets each instance clean itself up on the way out.

Neither finding mattered much on its own. Both came from the same discipline: when a page accepts input or holds a listener, someone should read that code the way an attacker or a leak detector would, even when the stakes look like zero.

The principle

Any place user input meets innerHTML is a finding, even when the exploit chain ends in your own tab. Static hosting removes whole classes of risk. It does not remove the ones you ship yourself.