Summary
users.email is a plain String field in backend/schemas/userModel.js with no unique index. The only thing stopping a second account on the same address is the findOne check at the top of registerController, and that check is a read followed by a write with no constraint behind it. Two requests that arrive at the same time both read "no such user" and both insert.
Once two rows share an address, every lookup in the app is findOne({ email }), which returns whichever document the storage engine hands back first. Login, /forgot-password, /reset-password and /verify-otp can each land on a different row than the user expects.
Expected result
- An email address identifies exactly one account.
- A duplicate registration is rejected by the database, not only by an application-level pre-check.
POST /api/user/register answers with a clear "user already exists" message even when two requests race.
Actual result
- Concurrent registrations create two accounts with the same email.
loginController does findOne({ email }).select("+password") and authenticates against an arbitrary one of them, so the same password can succeed or fail depending on which row is returned.
forgotPasswordController writes resetToken onto one row while resetPasswordController may read the other, and the reset silently fails with "Invalid or expired reset token".
- There is no index on
email at all, so every login is a collection scan.
Steps to reproduce
- Start the backend against a clean database.
- Fire two registrations for the same address at the same moment:
for i in 1 2; do
curl -s -X POST http://localhost:5000/api/user/register \
-H 'Content-Type: application/json' \
-d '{"name":"Race","email":"race@example.com","password":"password123","type":"student"}' &
done; wait
- Check the collection:
db.users.countDocuments({ email: "race@example.com" }) // 2
- Verify one of them, then try to log in a few times. The result flips between "Login success" and "Email is not verified".
Notes
- A fix needs to cover the data that is already there. Any existing deployment may already hold duplicates, so simply adding
unique: true will make index creation fail on startup with E11000. A dedupe step has to run first.
registerController should also translate a E11000 write error into the same 200/"User already exists" response it returns today, otherwise the race just moves from a duplicate row to an opaque 500.
- While the schema is being touched,
enrolledCourses, coursePayments and activityLogs are all queried by userId with no index either.
Summary
users.emailis a plainStringfield inbackend/schemas/userModel.jswith no unique index. The only thing stopping a second account on the same address is thefindOnecheck at the top ofregisterController, and that check is a read followed by a write with no constraint behind it. Two requests that arrive at the same time both read "no such user" and both insert.Once two rows share an address, every lookup in the app is
findOne({ email }), which returns whichever document the storage engine hands back first. Login,/forgot-password,/reset-passwordand/verify-otpcan each land on a different row than the user expects.Expected result
POST /api/user/registeranswers with a clear "user already exists" message even when two requests race.Actual result
loginControllerdoesfindOne({ email }).select("+password")and authenticates against an arbitrary one of them, so the same password can succeed or fail depending on which row is returned.forgotPasswordControllerwritesresetTokenonto one row whileresetPasswordControllermay read the other, and the reset silently fails with "Invalid or expired reset token".emailat all, so every login is a collection scan.Steps to reproduce
Notes
unique: truewill make index creation fail on startup withE11000. A dedupe step has to run first.registerControllershould also translate aE11000write error into the same 200/"User already exists" response it returns today, otherwise the race just moves from a duplicate row to an opaque 500.enrolledCourses,coursePaymentsandactivityLogsare all queried byuserIdwith no index either.