What a Year of CodeQL Alerts Looks Like on a Solo Project
Eight code-scanning alerts, four distinct classes, fixed one at a time across a year of commits — the real pattern is that rate limiting had to be fixed six separate times before it stuck.
git log --grep="code scanning alert" on the fishing-tracker repo returns eight commits. Six of them say "Missing rate limiting." That's not eight separate problems — it's one problem (no rate limiting middleware applied broadly enough) that CodeQL kept re-discovering every time a new route got added, plus two genuinely distinct vulnerability classes found once each. The list is a decent case study in what solo-maintainer security hygiene actually looks like when a scanner runs on every push.
The same alert class fired six times as new routes shipped — fix the middleware, not each route.
The workflow that makes this possible
CodeQL runs on every push to dev and main, every pull request, and once a week on a schedule — a fairly standard setup:
on:
push:
branches: [ "dev", "main" ]
pull_request:
branches: [ "dev", "main" ]
schedule:
- cron: '0 0 * * 0'
strategy:
matrix:
language: [ 'javascript-typescript' ]The weekly cron matters as much as the push trigger: alerts against existing code, not just new diffs, get raised even when nobody touches that file for months. That's how alert #159 (rate limiting, an early route) and alert #177 (rate limiting, a route added much later) both got caught without anyone having to remember to re-audit the whole API surface by hand.
Why "missing rate limiting" fired six times
CodeQL alerts on unrated endpoints per-route, not per-application — it has no concept of "this app has rate limiting somewhere." Alerts #159, #160, #164, #168, #171, and #177 each pointed at a different Express route that had no express-rate-limit middleware attached, spread across roughly two months of active development. The fix for each individual alert was trivial:
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });
router.post('/login', authLimiter, loginController);But applying it route-by-route, six separate times, is the wrong long-term shape of the fix — it means every new route starts unprotected by default and waits for CodeQL to notice. The right fix, which the project moved towards later, is a default rate limiter applied at the app level in server.js, with stricter overrides only on sensitive routes like /auth/login. Six individual alert-driven patches is what it looks like before you make that generalization; a global default is what it looks like after.
The other two alert classes
Client-side URL redirect (alert #183) — a redirect target built from a query parameter without validating it stayed within the app's own domain, the classic open-redirect pattern. Clear-text logging of sensitive information (alert #185) — a console.log or Winston log line that included a password or token field directly instead of redacting it first. Both are one-off bugs rather than systemic gaps, and both were fixed in single commits without follow-up patches, which is a useful contrast against the rate-limiting saga: some alert classes really are isolated; others are symptoms of a missing default.
What the pattern says about solo maintenance
Running CodeQL on a personal project without a security team behind it only pays off if you read the pattern across alerts, not just each one in isolation. Fixing six rate-limiting alerts as six unrelated tickets would have been technically correct and organizationally useless — it teaches you nothing about the next route you add. Fixing the first one, noticing the fifth one is the same shape, and generalizing the fix is the actual value a scanner provides to someone working alone: it's not just finding bugs, it's surfacing the missing convention.
Series: Fishing Tracker Pro. Next: a close read of alert #178 — a shell command built directly from environment variable values.