Skip to content

fix: security audit - patch vulnerabilities and regressions#236

Open
Yashagarwal9798 wants to merge 1 commit intoBitWebApp:mainfrom
Yashagarwal9798:security/fix-vulnerabilities
Open

fix: security audit - patch vulnerabilities and regressions#236
Yashagarwal9798 wants to merge 1 commit intoBitWebApp:mainfrom
Yashagarwal9798:security/fix-vulnerabilities

Conversation

@Yashagarwal9798
Copy link
Copy Markdown
Contributor

1. backend/package.json / backend/package-lock.json

What changed: Added two new security dependencies.

Package Version Purpose
helmet latest Sets secure HTTP headers (X-Frame-Options, X-Content-Type-Options, HSTS, etc.)
express-mongo-sanitize latest Strips $ and . from user input to prevent NoSQL injection attacks

2. backend/src/app.js

What changed:

  • Added helmet middleware with contentSecurityPolicy: false (disabled CSP to avoid breaking frontend inline scripts/styles; other headers like HSTS, X-Frame-Options are active).
  • Added express-mongo-sanitize middleware after body parsers to sanitize all incoming request data against NoSQL injection ({ "$gt": "" } style attacks).
  • Error handler middleware (errorHandler, notFoundHandler) was already imported but never mounted — now mounted at the bottom of the middleware chain.

What it solves:

  • Missing security headers (OWASP A05:2021 - Security Misconfiguration)
  • NoSQL injection via query/body parameters (OWASP A03:2021 - Injection)
  • Unhandled errors crashing the server or leaking stack traces

3. backend/src/controllers/admin.controller.js

What changed:

  • Cookie options hardened: Changed from { httpOnly: true, secure: false } to { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "Lax" } in both loginAdmin and logoutAdmin.
  • clearCookie fixed: logoutAdmin now passes matching cookie options to res.clearCookie() (browsers ignore clearCookie if options don't match the original set).
  • Typo fixed: .select("-password -refeshToken") corrected to .select("-password -refreshToken") in loginAdmin — the typo caused refreshToken to leak in login responses.
  • Data leak fixed: getCurrendAdmin now selects "-password -refreshToken" instead of just "-password".
  • Import moved: import { Otp } was placed mid-file (line 254) between function definitions — moved to the top of the file with other imports for proper module structure.

What it solves:

  • Cookies transmitted over HTTP in production (session hijacking)
  • CSRF via cross-site cookie submission (sameSite protection)
  • RefreshToken leaked in API responses (data exposure)
  • Logout not actually clearing cookies in browsers

4. backend/src/controllers/awards.controller.js

What changed:

  • IDOR protection added to deleteAward: Before deleting, verifies award.student.toString() === req.user._id.toString(). Users can only delete their own awards.
  • IDOR protection added to getAwardById: Verifies ownership before returning award data.
  • IDOR protection added to updateAward: Verifies ownership before allowing updates. Also removed student from destructured req.body — users cannot reassign awards to other students.

What it solves:

  • Any authenticated user could view/edit/delete any other user's awards by guessing the award ID (OWASP A01:2021 - Broken Access Control)

5. backend/src/controllers/exam.controller.js

What changed:

  • IDOR protection added to getExamById: Verifies exam.name.toString() === req.user._id.toString() before returning data (the name field is the User ObjectId reference).

What it solves:

  • Any authenticated user could view any other user's exam records by ID

6. backend/src/controllers/higher-education.controller.js

What changed:

  • IDOR protection added to getHigherEducationById: Verifies higherEducation.name.toString() === req.user._id.toString() before returning data.

What it solves:

  • Any authenticated user could view any other user's higher education records by ID

7. backend/src/controllers/internship.controller.js

What changed:

  • IDOR fixed in getInternshipDataforStudent: Previously accepted student_id from req.body (any user could pass any student's ID). Now uses req.user._id directly from the authenticated token.
  • Cloudinary URL fixed: Changed uploadedDoc.url and InternDocs.url to uploadedDoc.secure_url and InternDocs.secure_url — ensures HTTPS URLs are stored instead of HTTP.
  • Populated student data protected: Added "-password -refreshToken" to the .populate("student") call to prevent leaking sensitive fields.
  • Typo fixed: "Closuinary" corrected to "Cloudinary" in error message.

What it solves:

  • Any user could fetch any other student's internship data (IDOR)
  • Document URLs stored as HTTP instead of HTTPS
  • Password/refreshToken leaked through populated student references

8. backend/src/controllers/interview.controller.js

What changed:

  • Sort injection fixed: The sort query parameter was passed directly to MongoDB .sort(). Now validates against a whitelist of allowed sort fields: ["createdAt", "-createdAt", "company", "-company"]. Unrecognized values default to "-createdAt".
  • Removed debug console.log for companyId/studentId.

What it solves:

  • Attacker could inject arbitrary MongoDB sort expressions via query string (NoSQL injection variant)

9. backend/src/controllers/profProject.controller.js

What changed:

  • Ownership check in addNewProject: Uses req.professor._id instead of req.body.profId — prevents professors from creating projects under other professors' accounts.
  • Ownership check in editProject: Verifies project.professor.toString() === req.professor._id.toString() before allowing edits.
  • Ownership check in closeProject: Same ownership verification before closing.
  • Ownership check in updateApplicationStatus: Populates projectId on the application, then verifies the professor owns the referenced project. Added null guard for deleted projects to prevent TypeError.
  • Scope restriction in getAllApplications: Now queries only applications for the professor's own projects instead of returning all applications system-wide.

What it solves:

  • Professor A could edit/close/manage Professor B's projects (IDOR)
  • Professor could approve/reject applications for projects they don't own
  • Null dereference crash if a referenced project was deleted

10. backend/src/controllers/professor.controller.js

What changed:

  • Hardcoded IP removed: const url = "http://139.167.188.221:3000/faculty-login" changed to process.env.FACULTY_LOGIN_URL || "http://localhost:3000/faculty-login".
  • Auto-login URL also fixed: generateAutoLoginUrl had a second hardcoded IP http://139.167.188.221:3000/faculty-auto-login — changed to use process.env.FACULTY_LOGIN_URL.
  • Cookie options hardened: All three cookie-setting locations (loginProf, autoLoginProf, logoutProf) changed from secure: false to secure: NODE_ENV === "production", sameSite: "Lax".
  • clearCookie fixed: logoutProf now passes matching options to res.clearCookie().
  • Typo fixed: .select("-password -refeshToken") corrected to -refreshToken in loginProf.
  • Data leak fixed: getProf now selects "-password -refreshToken" — previously only excluded password, exposing all professors' refresh tokens to any caller.
  • OTP security: Math.random() replaced with crypto.randomInt(100000, 1000000) in otpForgotPassword — generates cryptographically secure 6-digit OTPs instead of weak 4-digit ones.
  • Authorization checks in accept/deny: All 6 accept/deny functions (summer, minor, major) now verify the professor is in the group's appliedProfs list before allowing accept/deny operations.
  • Pre-existing bug fixed: Student.updateMany() in mergeGroups changed to User.updateMany()Student model doesn't exist and would crash at runtime.
  • Pre-existing bug fixed: throw new ApiError("Invalid code...") in changePassword was missing the statusCode parameter — changed to new ApiError(400, "Invalid code...").

What it solves:

  • Hardcoded internal IP exposed in source and emails
  • Cookies insecure in production
  • RefreshToken leaked in login response and getProf endpoint
  • Weak/predictable OTPs (4-digit, Math.random)
  • Professor could accept/deny groups they're not assigned to
  • Runtime crash on mergeGroups (undefined Student model)
  • Incorrect error response format on invalid OTP

11. backend/src/controllers/user.controller.js

What changed:

  • crypto import added for crypto.randomInt().
  • OTP security (verifyMail): Math.floor(100000 + Math.random() * 900000) replaced with crypto.randomInt(100000, 1000000) — cryptographically secure 6-digit OTP.
  • OTP security (otpForgotPass): Same fix — Math.floor(Math.random() * 9000 + 1000) (4-digit, insecure) replaced with crypto.randomInt(100000, 1000000) (6-digit, secure).
  • isVerified check moved: In loginUser, the isVerified check now happens BEFORE generateAcessAndRefreshToken() — previously, tokens were generated and saved to DB even for unverified users.
  • Cookie options hardened: Both loginUser and logoutUser now use secure: NODE_ENV === "production", sameSite: "Lax".
  • clearCookie fixed: logoutUser passes matching options to res.clearCookie().
  • Data leak fixed: getCurrentUser now selects "-password -refreshToken -marks" instead of just "-marks".
  • Race condition handled: User.create() in registerUser wrapped in try-catch to handle MongoDB E11000 duplicate key errors gracefully (returns 409 instead of crashing with unhandled error).
  • IDOR fixed in getUserbyRoll: Removed isAdmin from req.body — admin/user role now determined by verifyAnyAuth middleware. Admin callers get full populated data; regular users get limited fields.

What it solves:

  • Predictable OTPs vulnerable to brute force (4-digit, Math.random)
  • Token generation wasted for unverified users
  • Session cookies insecure in production
  • Password and refreshToken leaked via getCurrentUser
  • Race condition on duplicate registration (TOCTOU → E11000 crash)
  • IDOR allowing students to access admin-level data via isAdmin body parameter

12. backend/src/middlewares/auth.middleware.js

What changed:

  • verifyAdmin DB check uncommented: Lines checking admin.isAdmin were commented out — now active. Prevents users with admin tokens but isAdmin: false from accessing admin routes.
  • verifyAnyAuth middleware added: New middleware that accepts User, Admin, or Professor JWT tokens. Tries Admin first (if token has isAdmin flag), then User, then Professor. Sets req.admin, req.user, or req.professor accordingly. Used for shared endpoints like /getbyroll, /getProf, /get-companies.

What it solves:

  • Admin role bypass (isAdmin flag in DB was never checked)
  • Shared endpoints that need to work for multiple user types couldn't use the type-specific middlewares without breaking one user type

13. backend/src/middlewares/multer.middleware.js

What changed: Complete rewrite of file upload configuration.

  • File type validation: Whitelist of allowed MIME types and extensions (JPEG, PNG, GIF, WebP, PDF, DOC, DOCX, XLS, XLSX). Both MIME type AND extension must match.
  • File size limit: 5MB max per file (limits: { fileSize: 5 * 1024 * 1024 }).
  • Secure filenames: Original filenames replaced with crypto.randomBytes(16).toString("hex") + original extension. Prevents path traversal and filename injection.

What it solves:

  • Unrestricted file upload (could upload .exe, .sh, .php, etc.)
  • No file size limit (DoS via large file uploads)
  • Original filenames preserved (path traversal, filename collision, information disclosure)

14. backend/src/models/otp.model.js

What changed:

  • TTL auto-expiry: Added createdAt field with expires: 600 (10 minutes). MongoDB automatically deletes OTP documents after 10 minutes via TTL index.
  • Removed invalid ref: email field had ref: "User" which is invalid on String fields (ref is for ObjectId references).
  • Removed unused import: import { User } was imported but never used.

What it solves:

  • OTPs never expired — valid indefinitely once created
  • Schema design violation (ref on non-ObjectId field)

15. backend/src/models/professor.model.js

What changed:

  • bcrypt rounds increased: Password hashing rounds changed from 10 to 12, matching the User and Admin models for consistency.

What it solves:

  • Professor passwords had weaker hashing than other user types

16. backend/src/routes/admin.routes.js

What changed:

  • Rate limiting on login: Added adminAuthLimiter (10 requests per 15 minutes) with requestIpMiddleware to the /login route.
  • /get-companies opened to all auth types: Changed from verifyAdmin to verifyAnyAuth — students need company list for the interview experiences page.
  • Imports updated: Added verifyAnyAuth, createRateLimiter, requestIpMiddleware.

What it solves:

  • Admin login brute force (no rate limit)
  • Students couldn't load company dropdown on interview experiences page

17. backend/src/routes/backlog.routes.js

What changed:

  • Added verifyAdmin to /add-subj: This route for adding backlog subjects had no authentication — anyone could add subjects.

What it solves:

  • Unauthenticated access to admin-only backlog management endpoint

18. backend/src/routes/classroom.routes.js

What changed:

  • Added verifyJWT to /bookedSlots: This GET route had no authentication — anyone could query all classroom booking information.

What it solves:

  • Unauthenticated access to classroom booking data

19. backend/src/routes/professor.routes.js

What changed:

  • Rate limiting on login: Added profAuthLimiter (10 req/15 min) to /login.
  • Rate limiting on OTP: Added profOtpLimiter (5 req/15 min) to /forgot-pass.
  • /getProf auth changed: From no auth → verifyAnyAuth (accessible by students, admins, and professors — all need the professor list).
  • Removed unused verifyJWT import.

What it solves:

  • Professor login brute force
  • OTP spam/abuse on forgot-password
  • Professor list publicly accessible without any authentication

20. backend/src/routes/user.routes.js

What changed:

  • Rate limiting on login: Added authLimiter (10 req/15 min) to /login.
  • Rate limiting on register: Added authLimiter to /register.
  • Rate limiting on OTP routes: Added otpLimiter (5 req/15 min) to /verifyMail and /get-pass-otp.
  • /getbyroll auth changed: From verifyJWT to verifyAnyAuth — admins need access to this endpoint for the dashboard student search.
  • Imports updated: Added verifyAnyAuth, createRateLimiter, requestIpMiddleware.

What it solves:

  • Login/register brute force
  • OTP flooding (unlimited OTP generation)
  • Admin dashboard student search broken by verifyJWT (only resolves User tokens)

21. backend/src/utils/Socket.js

What changed: Complete rewrite of Socket.IO server implementation.

  • JWT authentication middleware: io.use() middleware verifies JWT from socket.handshake.auth.token or Authorization header. Rejects unauthenticated connections. Resolves user's fullName from User or Professor collection during auth.
  • Room lookup fixed: Changed from Group.findById(roomId) (expects MongoDB ObjectId) to Group.findOne({ groupId }) (matches the nanoid string the frontend sends).
  • Membership check fixed: Changed from m.user.toString() (wrong — members are plain ObjectIds, not objects) to m.toString(), matching the existing chat.controller.js pattern.
  • Full role support: Membership check now covers leader, members, summerAppliedProfs, summerAllocatedProf, minorAllocatedProf, majorAllocatedProf — professors can now access chat rooms.
  • Sender spoofing prevented: Message sender is derived from socket.userFullName (resolved server-side during auth) — clients cannot impersonate other users. The sender remains a name string (compatible with existing ChatBox.jsx display logic).

What it solves:

  • Unauthenticated WebSocket connections (anyone could connect and listen)
  • Room join failures (findById on nanoid strings throws CastError)
  • Membership check failures (wrong property access on ObjectId array)
  • Professors locked out of chat (only members was checked)
  • Client-supplied sender field allowed impersonation

22. frontend/src/socket.js

What changed:

  • Auth token sent on connection: Added getAccessToken() helper that extracts the JWT from document.cookie (primary method) or falls back to localStorage.getItem("accessToken") (used by FacultyAutoLogin). Token is passed via auth: { token } in the Socket.IO connection options.
  • Removed duplicate reconnectionAttempts option.

What it solves:

  • Socket.IO connections rejected by the new server-side auth middleware (no token was being sent)
  • The previous localStorage.getItem("token") key was never set by any login flow

23. backend/src/models/professor.model.js (additional fix)

What changed:

  • Missing return before next() in pre-save hook: The isModified("password") guard called next() but didn't return, so execution fell through to bcrypt.hash(). Every .save() on a professor document (even for non-password fields) would re-hash the already-hashed password, corrupting it.

What it solves:

  • Professor passwords silently corrupted on any non-password update (e.g., profile edits, token rotation) — professors would be locked out after the next save

24. frontend/src/components/Login.jsx

What changed:

  • accessToken stored to localStorage after login: Added localStorage.setItem("accessToken", res.data.data.accessToken) after a successful login response.

What it solves:

  • Socket.IO client (socket.js) reads the token from localStorage as a fallback when httpOnly cookies are unreadable by JavaScript. Without this, WebSocket auth was always rejected for student logins.

25. frontend/src/components/Loginadmin.jsx

What changed:

  • accessToken stored to localStorage after login: Same fix as Login.jsx — localStorage.setItem("accessToken", res.data.data.accessToken).

What it solves:

  • Admin WebSocket connections failed for the same reason (no token in localStorage, httpOnly cookie unreadable by JS)

26. frontend/src/components/LoginFaculty.jsx

What changed:

  • accessToken stored to localStorage after login: Same fix — localStorage.setItem("accessToken", res.data.data.accessToken).

What it solves:

  • Faculty WebSocket connections failed (FacultyAutoLogin already stored the token; this aligns the manual login flow)

27. frontend/src/components/Header.jsx, HeaderFaculty.jsx, HeaderAdmin.jsx, Sidebar.jsx, SideBarFaculty.jsx, SideBarAdmin.jsx

What changed:

  • accessToken cleared from localStorage on logout: Added localStorage.removeItem("accessToken") in all 6 logout handlers, immediately after the existing localStorage.removeItem("user") / localStorage.removeItem("faculty") calls.

What it solves:


28. backend/src/middlewares/auth.middleware.js (additional fix)

What changed:

  • optionalAuth middleware added: Similar to verifyAnyAuth but non-blocking — if a valid token is present, it resolves req.admin, req.user, or req.professor; if no token or an invalid token is present, it silently calls next() with no user attached (instead of returning 401).

What it solves:

  • The /getbyroll endpoint (student dashboard) needed to work for both authenticated admins (full data) AND unauthenticated/public visitors (limited data). verifyAnyAuth rejected unauthenticated requests with 401, breaking the public dashboard.

29. backend/src/routes/user.routes.js + backend/src/routes/admin.routes.js (additional fix)

What changed:

  • /getbyroll switched from verifyAnyAuthoptionalAuth: Allows unauthenticated access with limited data, while admins still get full populated data.
  • /get-companies made fully public: Removed verifyAnyAuth middleware entirely — company names are non-sensitive and needed by the interview experiences page filter dropdown without requiring login.
  • Removed unused verifyAnyAuth import from admin.routes.js after the change.

What it solves:

  • Public student dashboard page returned 401 for unauthenticated visitors (regression from original verifyAnyAuth change)
  • Interview experiences page company filter dropdown failed for non-logged-in users (regression from original verifyAnyAuth change)

30. backend/src/controllers/professor.controller.js + backend/src/cron-jobs/notifyProf.js, notifyProfMinor.js, notifyMajorProf.js (additional fix)

What changed:

  • Separated FACULTY_LOGIN_URL and FACULTY_AUTO_LOGIN_URL: The original fix (Login and Signup Added #10) used a single FACULTY_LOGIN_URL env var for both the manual login link and the auto-login token URL. These are two different pages (/faculty-login vs /faculty-auto-login) — a single variable caused one to always be wrong. Now uses FACULTY_AUTO_LOGIN_URL (with fallback http://localhost:3000/faculty-auto-login) for auto-login links.
  • Auto-login token expiry fixed: Changed from "12h" to "30m" — a 12-hour auto-login token is excessive; 30 minutes matches the intended design (as noted in the original code comment).
  • Cron jobs updated: All 3 cron files (notifyProf.js, notifyProfMinor.js, notifyMajorProf.js) had 2 hardcoded 139.167.188.221 URLs each (6 total). Replaced with process.env.FACULTY_AUTO_LOGIN_URL and process.env.FACULTY_LOGIN_URL respectively.

What it solves:

  • Auto-login links in cron emails pointed to the wrong page (manual login instead of auto-login)
  • 12-hour auto-login tokens were an excessive window for a single-use link sent over email
  • 6 remaining hardcoded internal IPs in cron jobs (missed in the original pass)

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 12, 2026

@Yashagarwal9798 is attempting to deploy a commit to the bitwebapp's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Suryansh-Dey Suryansh-Dey force-pushed the main branch 2 times, most recently from ffd2a0e to a5e184e Compare March 12, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant