A static site generator seems like the safest possible software. No database. No user authentication. No server-side processing. Markdown goes in, HTML comes out. What could go wrong?

Quite a lot, as it turns out. I spent part of today running a systematic security audit on the Python code that builds this blog, and the results were sobering. Forty-five issues across six severity categories, ranging from XSS vulnerabilities in the search functionality to race conditions in file operations. The code has been running in production for months. Every issue had been hiding in plain sight.

The most serious problem was a classic cross-site scripting vulnerability. The search feature highlights matching text by inserting content directly into the DOM via innerHTML — a pattern that the OWASP DOM-based XSS Prevention Cheat Sheet explicitly warns against. If a malicious post title contained script tags, the search results would execute them. The fix was straightforward: escape HTML entities before highlighting. However, the vulnerability should never have existed in the first place.

Race conditions appeared in several places. The build script called os.listdir() twice for duplicate detection — once to build a normalisation map, again to process files. Between those calls, the filesystem could change. The cache file for URL shortening used a naive write pattern that could corrupt data during concurrent builds. The asset copying routine deleted the entire output directory before recreating it, creating a window where the site would be unavailable. Each fix required thinking carefully about atomicity and the assumptions that file operations make about a static world.

Date arithmetic revealed a subtler class of bug. The time-based filtering used timedelta(days=months * 30) to calculate cutoff dates — a calculation that drifts by five or six days over a year. Posts from exactly twelve months ago might or might not appear depending on which months fell within the range. The dateutil library provides relativedelta specifically to handle calendar arithmetic correctly. There was no excuse for not using it.

Path traversal prevention was missing entirely. A crafted slug containing ../ could write files outside the output directory. Input validation existed for character sanitisation but not for structural attacks. The oversight was embarrassing.

What strikes me most is that this code was written with agentic coding tools — the same tools that are supposed to bring senior-level expertise to every developer. The tools generated working code that passed all tests and produced correct output. They did not generate secure code. They did not flag the race conditions or the XSS vulnerability or the date arithmetic error. The code worked, which is a different thing from the code being right.

This reinforces something I have been thinking about: no system can verify its own blind spots. The AI that helped write the code could not see what it had missed. The developer reviewing the output — me — did not catch the issues either. Only a deliberate, adversarial audit with a checklist of known vulnerability patterns found what was hiding in plain sight.

The fixes took a few hours. The lesson will last longer. Safe-looking code is not the same as safe code. Static sites are not immune to security issues. And the tools that accelerate development do not eliminate the need for the slow, careful work of verification.

Sources: