I maintain CodeDiag, a CLI that scans a Node.js project and tells you which API routes have no recognizable auth check or input validation. It's deterministic: no LLM, no account, and with --no-audit it never touches the network.
Before releasing 0.10, I pointed it at five well-known open-source apps to test new SvelteKit and React Router support. Here is what it said about Epic Stack, one of the most carefully built full-stack starters around:
Score: F (43/100)
Security: 0/100 — 6 critical findings
Epic Stack is not a zero. My scanner was. This post walks through every false positive I found, what I changed, and what is still wrong.
The false positives
Every one of these was reported as critical:
| The code | What CodeDiag 0.8 said |
|---|---|
password: "Password is required" — a validation error message |
Hardcoded secret |
typeof password !== "string" — a form-data type check |
Plaintext password comparison |
confirmPassword !== password inside a Zod superRefine
|
Plaintext password comparison |
createHash('sha1') building a Pwned Passwords range-API prefix |
Password hashed with SHA-1 |
GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET" in .env.example
|
Committed secret (twice) |
const password = 'testpassword' in a test |
Hardcoded secret |
| Epic Stack serves React Router from an Express server | "No Express routes found" → API Health 0/100 |
On the Remix indie-stack, the first two rows alone produced four critical findings: the sign-up and login forms each have one typeof check and one "Password is required" message. The Lucia SvelteKit example got four "plaintext password comparisons" for four typeof checks.
All of them share a root cause: rules that were too lexical. They saw the word "password" next to a string literal or a !== and stopped thinking.
What changed
A secret needs a value that looks like a secret. The pattern now captures the value and rejects anything with whitespace ("Password is required") or a placeholder (MOCK_, your-…, changeme, ${VAR}). Test files are skipped for this rule.
Password comparison is an AST check now. It looks at equality operators where one side is a password value. It ignores typeof checks, constants, .length checks, and confirm/repeat/retype fields.
Pwned Passwords is a known pattern. SHA-1 used to build a k-anonymity prefix for the range API is not password storage.
Stack detection looks at what serves the routes. Express in front of React Router is a React Router app.
The opposite bug: routes marked safe that weren't
False positives are loud. False negatives are worse, because nobody notices them. In 0.8, if your Next.js middleware.ts mentioned a session, every API route counted as protected. So this middleware made POST /api/posts look safe:
export function middleware(request: NextRequest) {
const session = request.cookies.get("session")?.value;
if (!session && request.nextUrl.pathname.startsWith("/api/admin")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.next();
}
0.10 evaluates the middleware for an anonymous request to each route:
- the
matcher; -
pathname.startsWithand similar branches; - 401 responses and redirects to sign-in;
- the Auth.js
authorizedcallback; - Supabase's
updateSession(), followed into the project helper.
Here /api/admin/* is protected and POST /api/posts is reported. The same evaluation catches a common surprise: clerkMiddleware() on its own protects nothing. Clerk leaves every route public until you call auth.protect().
The new framework support models three similar traps:
-
SvelteKit: an auth check in
+layout.server.tsdoesn't protect form actions. Actions run before load functions, and+serverendpoints never run a layout load at all. - React Router / Remix: a parent route's loader doesn't protect a child's action. A form submission runs the action first, and loaders run in parallel.
-
tRPC: auth in
createContext, or middleware in front of the adapter mount, covers every procedure. That holds only if every adapter that serves the router is covered.
Results
Same five apps, published npm versions, --no-audit:
| App | Score 0.8 → 0.10 | Security 0.8 → 0.10 | Critical |
|---|---|---|---|
| Epic Stack | 43 → 86 | 0 → 72 | 6 → 0 |
| Remix indie-stack | 53 → 87 | 3 → 78 | 5 → 1¹ |
| Lucia SvelteKit email/2FA example | 59 → 83 | 53 → 100 | 5 → 1² |
| sveltejs/realworld | 71 → 78 | 86 → 100 | 1 → 1² |
| shadcn/taxonomy (Next.js) | 80 → 80 | 100 → 100 | 1 → 1² |
¹ No lock file in the template. ² No test files. Neither is a security issue.
What's still wrong
-
Epic Stack
server/index.ts:50: CodeDiag still warns about an open redirect in the trailing-slash redirect. The code collapses repeated slashes first (.replace(/\/+/g, '/')), which is exactly the defense against//evil.com. CodeDiag doesn't recognize that sanitizer yet. -
Epic Stack API Health: Epic Stack uses
remix-flat-routes, which CodeDiag doesn't read, so it gets no API Health score. -
indie-stack warnings: the remaining ones sit in a Cypress helper and the one-time
remix.initscript. They're technically right and practically noise. - The general limit: "no recognizable auth check" means exactly that. If your auth lives somewhere CodeDiag can't see, it will say so. Suppressions and baselines exist for those cases.
Try it
npx @sabahattink/codediag scan .
Or in CI, with a PR comment:
- uses: sabahattink/codediag@v0
with:
threshold: 80
comment: true
If it's wrong about your code, I want to see it. Open an issue with the snippet. Every fix above started as a finding that didn't hold up.
Top comments (0)