Ominvo beta launches July 23, 2026 — only 10 spots remain Join the waitlist →
All posts
Building Ominvo

I found 26 dead links in production while trying to fix a security bug that didn't exist

July 8, 20265 min read

The day started with one item on the punch list: a MEDIUM-severity CORS finding from Day 74's blackbox security audit, sitting underneath the same session where yesterday's post covered the 4.5-star cliff. Fix the header, close the finding, move on. It didn't go that way. By the end of the day I'd shipped two commits — one for a vulnerability that turned out not to exist, and one for 26 dead links I wasn't looking for when I found them. Both showed up because I checked something in production instead of trusting what the code implied. Same discipline, two different points in the pipeline: one on the way in, before I wrote a fix, and one on the way out, after a fix had already shipped.

The CORS wildcard that wasn't

The audit flagged access-control-allow-origin: * site-wide and rated it MEDIUM. My first instinct was to write the fix prompt straight away — scope CORS to the specific origins that need it on /api/*, ship it, close the finding. Before writing that prompt, I curled production directly instead: root plus five API routes, checking response headers by hand.

The wildcard was real, but only on cached prerendered content served off Vercel's edge network — the kind of header a CDN sets on static HTML by default, not something the app configured. Every /api/* endpoint I checked came back with zero CORS headers set at all. No wildcard, no scoping needed, because there was nothing there to scope. Non-finding.

I killed the CORS fix before writing a line of it and pivoted to the audit's other flag: missing hardening headers, rated LOW. That one was real. Four headers, one next.config.ts block — X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and a Permissions-Policy locking camera, microphone, and geolocation. Shipped it.

The 404 that turned out to be a Day 70 bug

After the headers shipped, I did what I always do before calling something done: I dogfooded it end-to-end instead of trusting the diff. That meant curling a live Content Library article to confirm the new headers were actually present on it. I curled the reputation-math article and got back a 404, with x-matched-path: /404 in the response.

First read: regression, caused by today's headers commit. I nearly started writing a rollback prompt on that assumption. Second read, before touching anything: I checked the sitemap and the rendered hub HTML instead of trusting my memory of the URL. The real article lived at the flat path — no /the-numbers/ segment in it anywhere. The URL I'd been curling had never resolved to anything, headers commit or not. It had been sitting in the Day 74 handoff's pre-flight spot-check as the canonical article URL, which meant the wrong shape had already made it into written documentation once. That's what made me stop assuming it was a one-off typo and go look at everywhere else the same shape might be hiding.

How the audit sweep found 26 dead links

I wrote an audit prompt: read all five Content Library articles, extract every internal link, classify each as clean or wrong-shape. Eleven broken sibling links turned up inside the five articles — every single article was cross-linking to at least two neighbors using a URL shape that had never worked.

I widened the sweep across the Content Library and out to the rest of content/ and src/. Fifteen more hits: ten inside the Day 69 through Day 74 blog posts, five more sitting as literal strings inside changelog entry copy. Root cause was consistent everywhere: the CC prompts across Days 70 through 74 had specced the file path — category folder plus slug — as if it were the URL, when the live route is flat. Nobody had caught it because nobody had clicked the links in production; they render fine as blue text regardless of where they point.

Twenty-six dead URLs, live for five days, discovered by accident while checking a header. I bundled the fix into one commit: 26 mechanical rewrites, URL shape only, no prose touched, no anchor text changed. Verified every diff and grepped site-wide for the broken shape before committing — zero matches left.

What I'm carrying forward

Verify-before-fix isn't only about catching false findings before they cost a commit. It's about the discoveries that only surface once you dogfood the fix end-to-end instead of stopping at "the diff looks right." If I'd shipped the CORS scoping without curling first, I'd have written real code against a problem that didn't exist. If I'd shipped the hardening headers without curling a live article afterward to confirm them, the 26 dead links would still be sitting in production tonight, unnoticed.

Compiling clean and passing the type checker is table stakes. It says nothing about whether an article's own sibling links resolve. Those broke not because of a code defect but because nobody had clicked them. Two saves in one day, opposite ends of the same pipeline, same habit both times. Full detail on both fixes is in the changelog.

Written by

The founder of Ominvo

Building review management for single-location small businesses. Join the waitlist →