fix(nextjs-backend): patch secret leak and invalid CORS header; harden users API - #13
fix(nextjs-backend): patch secret leak and invalid CORS header; harden users API#13frankstupak wants to merge 1 commit into
Conversation
|
CI has now run on this branch for the first time — the earlier runs sat unapproved (fork PR, first-time-contributor policy) and expired, so nothing was ever reported here. Result: failure on Node 22, in this PR's own route. Node 18 and 20 were cancelled once 22 failed.
The optional parameter is the problem: export async function GET(request?: NextRequest): Promise<NextResponse> {
const params = request?.nextUrl?.searchParams;Next validates route exports structurally, and an optional first parameter widens the type to include Making the parameter required is the fix on the route side: export async function GET(request: NextRequest): Promise<NextResponse> {
const params = request.nextUrl.searchParams;and the callers construct one, e.g. Worth knowing before you re-push: |
|
Status update. Two things had been masking the real verdicts:
This one is now the only thing standing between the branch and a merge — the failure is in your own change, detailed in the comment above. Once it is fixed, rebase or use "Update branch" so CI runs against current Also filed #15 for a flake you may hit on a re-run: |
…PI, O(1) dedupe Security - next.config.js `env` no longer inlines JWT_SECRET / DATABASE_URL into the build (Next embeds `env` values in the JS bundle at build time). Server code reads them from process.env directly. - Replaced the static Access-Control-Allow-Origin header (set to the raw, comma-separated ALLOWED_ORIGINS — an invalid ACAO value browsers reject) with origin-reflection CORS in middleware.ts: match the request Origin against the allow-list, reflect a single valid origin, add Vary: Origin, answer OPTIONS preflight with 204, and never emit `*` with credentials. - X-XSS-Protection set to "0" (OWASP), added Referrer-Policy: no-referrer. Correctness (POST /api/users) - Reject duplicate emails with 409 Conflict (was silently creating dupes). - Normalize email (trim + lowercase) so case/whitespace variants are one identity. - Reject whitespace-only names (plain z.string().min(1) accepted " "). Performance - Email uniqueness backed by an O(1) Map index instead of an O(n) Array.find scan (which would be O(n^2) across N inserts). Bench: naive vs indexed — 559ms->4.4ms @10k (127x), 30,901ms->25.6ms @50k (1209x). Extras - Optional, backward-compatible pagination on GET /api/users (?limit&offset); omitted params return the full list unchanged; total stays the full count. - +23 tests (9 -> 32): cors unit tests, users dedupe/normalize/trim/pagination, and a next.config regression guard. tsc + eslint clean.
592b2a0 to
5c2b457
Compare
|
Rebased onto main and both checks are green now. Ready for review whenever you get a chance. |
|
Sorry for the delay — you cleared this on Aug 30 and it sat. Confirmed green: Verified the fix matches the description: -export async function GET(): Promise<NextResponse> {
+export async function GET(request: NextRequest): Promise<NextResponse> {Your diagnosis is right, and it's worth stating why for anyone reading this later: App Router type-checks each Heads up on a possible collision with #10. That PR adds a brand-new Nothing needed from you here. This one's ready and I'll review the CORS and users-API hardening properly next — 9 files with real tests ( |
nextjs-backend uplift — secret leak, invalid CORS header, hardened users API, O(1) dedupe
The
nextjs-backendreference had two shipped-config bugs that would bite in production, an unguarded users endpoint, and a benchmarkable perf trap waiting for anyone who added dedupe the obvious way. Fixed all four, backward-compatibly, with tests to keep them fixed.🔒 Security — two real config bugs
1. Secrets were being inlined into the client bundle.
next.config.jsexposedJWT_SECRETandDATABASE_URLthrough theenvkey. Per the Next.js docs, values inenvare always included in the JavaScript bundle at build time — so the JWT signing secret shipped to the browser. Removed them; server code already readsprocess.env.JWT_SECRET/process.env.DATABASE_URLdirectly in route handlers and server components.2. The CORS header was invalid. The static header block set
Access-Control-Allow-Origintoprocess.env.ALLOWED_ORIGINS, whichenv.exampledocuments as a comma-separated list (http://localhost:3000,http://localhost:3034).Access-Control-Allow-Originis not list-valued — the spec allows a single origin,*, ornull. A comma/space list is rejected by every browser, so cross-origin requests from the "allowed" origins would fail outright.The correct pattern for a multi-origin allow-list is per-request origin reflection, which a static header block can't do. Moved CORS into
src/middleware.ts:Originagainst the allow-list and reflect back a single valid origin,Vary: Originso shared caches don't cross-serve,OPTIONSpreflight with204,*together with credentials (also spec-illegal).All the origin-decision logic lives in
src/lib/cors.tsas pure functions with exhaustive unit tests. Also setX-XSS-Protection: 0(current OWASP guidance — the legacy auditor can introduce vulnerabilities) and addedReferrer-Policy: no-referrer.✅ Correctness —
POST /api/users409 Conflictinstead of silently creating duplicate records (the Prisma schema marksemail @unique; the in-memory store ignored it).Person@Example.comandperson@example.comare one identity.z.string().min(1)accepted" "; now.trim().min(1).⚡ Performance — O(1) uniqueness, no O(n²) trap
Email uniqueness is backed by a
Mapindex (O(1) per insert) rather than the obvioususers.find(u => u.email === email)scan, which is O(n) per insert → O(n²) across N inserts. Benchmark (bench/dedupe-bench.ts,npx tsx):Array.findMap➕ Extras
GET /api/users(?limit&offset). No params → the full list, unchanged shape;totalalways reflects the full count. (The subproject even definedPaginatedResponse<T>but never used pagination.)CONFLICT: 409to theHttpStatusconstants (the file's stated purpose is replacing magic numbers).🧪 Verification
+23). New: CORS unit tests, users dedupe/normalization/trim/pagination, and anext.configregression guard (asserts no secrets inenv, no static ACAO,X-XSS-Protection: 0).nextjs-backendjest project (run via the repo's root jest config): 32/32 green.tsc --noEmitclean;eslint --max-warnings 0clean.src/api/nextjs-backend/; the root default jest project excludes that path by config, so nothing else is affected. Public API/response shapes unchanged.— Lumen Industries