Repository navigation
feat(api): restrict CORS origins and add security headers to deployed serverless API - #1754
Merged
1nonlypiece merged 2 commits intoOct 24, 2026
Conversation
… serverless API Closes Disciplr-Org#1731 Restrict CORS on write endpoints via CORS_ORIGINS allow-list while keeping public GET routes accessible cross-origin. Add Helmet security headers (CSP, HSTS, no-referrer, nosniff) across all responses, and fail closed in production when unset.
|
@Stephan-Thomas Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
| */ | ||
| export function normalizeOrigin(origin) { | ||
| if (typeof origin !== 'string') return '' | ||
| return origin.trim().replace(/\/+$/, '') |
…flicts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1731
src/app.tstoapi/index.mjsproviding baseline security headers (X-Content-Type-Options: nosniff,Strict-Transport-Security1-year with subdomains,Referrer-Policy: no-referrer, and restrictive API CSPdefault-src 'none',frame-ancestors 'none') across all responses including 200, 404, and 500.apilib/cors.mjscontaining:buildCorsOptions(env): creates CORS options with explicit methods (GET,POST,OPTIONS), allowed headers (Content-Type,Authorization,idempotency-key,x-test-now,x-wallet-address,x-network-id),maxAge: 600, andcredentials: false.parseAllowedOrigins(env): parses comma-separatedCORS_ORIGINSfrom environment, trims whitespace, normalizes trailing slashes, and filters empty entries.CORS_ORIGINSin production (NODE_ENV === 'production') resolves to[](rejecting cross-origin writes with 403 Forbidden). In development/test, defaults tohttps://disciplr-frontend.vercel.app.isOriginAllowed(origin, allowedOrigins): exact origin comparison (no substring or regex matching) to avoid vulnerabilities with malicious domains.api/index.mjs:*) access for public read routes (GETandHEADon/health,/openapi.json,/vaults,/transactions,/stats).OPTIONSpreflight (returns matchingAccess-Control-Allow-Originfor allowed origins; no CORS headers for disallowed origins) andPOSTwrite routes (returns 403 JSON{ error: 'Origin not allowed' }without mutating store state).Originheader (server-to-server, curl) continue to be allowed..env.example: documentCORS_ORIGINSfor serverless API (default frontend origin and fail-closed production behavior).README.md: documentCORS_ORIGINSin environment variables table and deployment notes.api/__tests__/cors.security.test.mjscovering unit tests forbuildCorsOptions& origin parsing, preflight allowed/denied matrix, POST allowed/denied/missing origins (asserting store count unchanged on denial), GET cross-origin, and presence of all security headers on 200/404/500 responses.npm run test:api.Why
The production serverless API previously called
app.use(cors())with no options, exposing state-changing POST routes (/vaults,/validate,/fail) cross-origin to any website a victim visits withAccess-Control-Allow-Origin: *. Furthermore, no standard security headers (CSP, HSTS, nosniff, Referrer-Policy) were present on responses. Restricting cross-origin writes to configured frontend origins and attaching hardened headers mitigates cross-site scripting and unauthorized cross-origin request risks.Testing
Run commands:
node --test api/__tests__/cors.security.test.mjs(18 passed, 0 failed)npm run test:api(51 passed, 0 failed, 4 skipped)npm run build(clean TypeScript build)npm run openapi:check(docs/openapi.yaml up to date)Test coverage details:
normalizeOriginstrips whitespace and trailing slashes.parseAllowedOriginsparses comma-separated origins, ignores wildcard*for writes, and fails closed in production when unset.isOriginAllowedchecks exact origin matching without substring or regex false positives.buildCorsOptionsreturns expected shape, methods, headers, and origin callback logic.OPTIONS /api/vaults: allowed origin returns matchingAccess-Control-Allow-Origin: <origin>and max-age 600; disallowed origin returns no CORS header./api/vaults: disallowed origin returns403JSON and store count is not mutated; allowed origin returns201and adds vault; missing origin (curl/server-to-server) returns201./validateand/fail: disallowed origin returns403JSON.CORS_ORIGINSwithNODE_ENV: productionrejects cross-origin POSTs.GET /api/health,/api/vaults,/api/stats,/api/transactions,/api/openapi.json) returnAccess-Control-Allow-Origin: *.X-Content-Type-Options: nosniff,Strict-Transport-Security,Referrer-Policy: no-referrer, andContent-Security-Policy) verified on 200, 404, and 500 responses.Scope
In scope: CORS origin policy split for serverless API, Helmet security headers,
apilib/cors.mjshelper,.env.example,README.md, and integration test suite inapi/__tests__/cors.security.test.mjs.Out of scope: Authentication/wallet signatures on write endpoints (separate issue); rate limiting (separate issue); changes to
src/app.ts.