Fix critical auth vulnerabilities: rate limiting, session fixation, CORS, cookie hardening#377
Conversation
…flags - Add express-rate-limit (10 req/15 min/IP) on /api/auth/login and /api/auth/signup - Regenerate session ID after successful login to prevent session fixation - Strip password hash from deserializeUser and login response - Unify auth error messages to prevent user enumeration - Replace wildcard CORS with explicit ALLOWED_ORIGINS allowlist - Add httpOnly, Secure, SameSite, maxAge flags to session cookie - Strip internal error details from 500 responses - Set NODE_ENV=production in production Dockerfile - Document ALLOWED_ORIGINS and NODE_ENV in .env.sample Closes GitMetricsLab#372, GitMetricsLab#373, GitMetricsLab#374, GitMetricsLab#375
✅ Deploy Preview for github-spy ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Caution Review failedPull request was closed or merged during review No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR hardens the backend authentication and API security by addressing four linked vulnerabilities: wildcard CORS, missing session cookie security flags, absent rate limiting on auth endpoints, and session fixation/password hash leaks. Environment variables, Passport strategy, session handling, rate limiting, CORS restrictions, and authentication routes are updated in concert. ChangesBackend Security Hardening
Sequence DiagramsequenceDiagram
participant Client
participant RateLimit as Rate Limiter
participant CORS
participant LoginRoute as /api/auth/login
participant Passport
participant Session as Session Regenerate
participant UserDB as User Database
Client->>RateLimit: POST /api/auth/login
RateLimit->>CORS: Check if within limit
CORS->>CORS: Verify origin in allowlist
CORS->>LoginRoute: Request approved
LoginRoute->>Passport: passport.authenticate('local')
Passport->>UserDB: Find user by email
alt User found
Passport->>Passport: Verify bcrypt password
alt Password matches
Passport->>LoginRoute: User valid
LoginRoute->>Session: req.session.regenerate()
Session->>Client: Issue new httpOnly, secure, sameSite cookie
LoginRoute->>LoginRoute: req.logIn(user)
LoginRoute->>Client: 200 {id, username, email}
else Password mismatch
Passport->>LoginRoute: Invalid credentials
LoginRoute->>Client: 401 {message: "Invalid credentials"}
end
else User not found
Passport->>LoginRoute: Invalid credentials
LoginRoute->>Client: 401 {message: "Invalid credentials"}
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🎉 Thank you @advikdivekar for your contribution. Please make sure your PR follows https://github.com/GitMetricsLab/github_tracker/blob/main/CONTRIBUTING.md#-pull-request-guidelines
|
Superseded by four focused PRs — one per security issue — to make review and merging easier. |
What is the problem
The backend authentication layer had four exploitable security vulnerabilities:
/api/auth/loginand/api/auth/signup— unlimited brute-force and credential-stuffing with no server-side resistance.passport.authenticatestored the authenticated user into the pre-existing session ID. An attacker who plants a known session ID in the victim's browser becomes authenticated after the victim logs in.cors('*')) — any origin could read API responses, permanently exposing all current and future endpoints with no review step.express-sessionwas configured withouthttpOnly,Secure, orSameSite, making the session ID readable via JavaScript and transmittable cross-site.Two secondary issues fixed in the same pass: the bcrypt hash was leaking in the login response via
req.user, and distinct error messages ('Email is invalid'vs'Invalid password') enabled user enumeration.What was changed
backend/server.jscors('*')with an explicitALLOWED_ORIGINSallowlist; addedexpress-rate-limit(10 req / 15 min / IP) on/api/auth/loginand/api/auth/signup; addedhttpOnly,secure,sameSite, andmaxAgeto the session cookie configbackend/routes/auth.jspassport.authenticate; callsreq.session.regenerate()beforereq.logIn()to issue a fresh session ID; returns only{ id, username, email }— neverreq.user; strippederr.messagefrom all 500 responsesbackend/config/passportConfig.js'Invalid credentials'to prevent user enumeration; added.select('-password')todeserializeUserso the hash is never loaded intoreq.useron subsequent requestsbackend/package.jsonexpress-rate-limit ^7.5.1as a production dependencybackend/.env.sampleNODE_ENVandALLOWED_ORIGINS; replaced placeholder secret hintbackend/Dockerfile.prodENV NODE_ENV=productionso production cookie flags (Secure,SameSite=Strict) activate automaticallyWhy this approach
Each fix targets the root cause, not just the symptom:
req.logIn(), which is the only safe ordering. After regeneration the old session is destroyed server-side and the new session is empty;logInthen writes the user ID into the fresh session. Doing it after would leave a window where the old session holds the authenticated state.SecureandSameSite=Strictin production,laxin development so local HTTP still works without disabling cookies entirely.How to test
Rate limiting — Send 11 consecutive
POST /api/auth/loginrequests with wrong credentials from the same IP. The 11th must return429 Too Many Requestswith the message"Too many attempts, please try again after 15 minutes."A successful login on attempt 5 should reset the counter.Session fixation — Record the
connect.sidcookie before login (any unauthenticated request). Log in with valid credentials. TheSet-Cookieheader in the login response must carry a differentconnect.sidvalue.Password hash not leaked — The
POST /api/auth/loginsuccess response must contain exactly{ message, user: { id, username, email } }. Nopasswordfield at any level.CORS — From a browser tab at
http://evil.com, runfetch('http://localhost:5000/api/auth/login', { method: 'POST', ... }). The browser must throw a CORS error. Fromhttp://localhost:5173the same request succeeds.Cookie flags — After login, open DevTools → Application → Cookies.
connect.sidmust showHttpOnly ✓. In production (NODE_ENV=production) it must also showSecure ✓andSameSite: Strict. Runningdocument.cookiein the console must return"".User enumeration closed —
POST /api/auth/loginwith a non-existent email and with an existing email but wrong password must both return{ "message": "Invalid credentials" }— identical body, identical status code.Edge cases covered
!originguard allows server-to-server requests (health checks, internal services) through without needing them on the allowlist.ALLOWED_ORIGINSfalls back tohttp://localhost:5173when the env var is absent, so no config change is required for existing local dev setups.skipSuccessfulRequests: trueon the rate limiter means a user who provides correct credentials on attempt 3 does not consume further quota; only failures count.sameSite: 'lax'in non-production keeps OAuth redirect flows and local development working while still blocking third-party POST CSRF.secure: falsein dev prevents cookie loss when the dev server runs on plain HTTP.regenerateErrandloginErrare forwarded to the Express error handler vianext()so they surface as 500s rather than silent hangs.err.message) are removed from all 500 responses (signup, login error path, logout) to prevent leaking MongoDB internals.Verification
spec/does not include rate limiter, so test suite is unaffected by the new middleware)Labels:
type:securitylevel:advancedgssoc:approvedCloses #372
Closes #373
Closes #374
Closes #375
Please assign this PR to me under GSSoC 2026.
Summary by CodeRabbit
Release Notes
Bug Fixes
Chores