Login identifier column has no unique constraint
- Rule:
schema-login-identifier-not-unique - Default severity: Medium
- CWE: CWE-287 (Improper Authentication)
- Risk category: Authentication
A login table (it stores a password) has an email or username column with no unique constraint, and the code looks accounts up by that column. Two accounts can then share one login identifier: a sign-up race or a case-variant duplicate makes the lookup return whichever row the database finds first. That can let one user sign in to, reset the password of, or receive mail for another user's account.
What Vulkro detects
Vulkro reads every model and migration in the database schema for the table. The rule fires when no unique constraint or unique index covers the login identifier, and the code looks an account up by it.
Remediation. Add a unique constraint (or unique index) on the login
identifier, case-insensitive where the database supports it (citext, a
lower(email) unique index, COLLATE NOCASE), normalise the value before
writing it, and remove duplicate rows before the migration runs. A check in
the application alone is not enough: only the database closes the race.
Non-compliant code (example)
// `email` is the login identifier but carries no unique constraint.
const accountSchema = new mongoose.Schema({
email: { type: String, required: true, lowercase: true },
passwordHash: { type: String, required: true },
});
// Accounts are looked up by email at sign-in.
const account = await Account.findOne({ email: req.body.email.toLowerCase() });
Compliant code (example)
const accountSchema = new mongoose.Schema({
email: { type: String, required: true, lowercase: true },
passwordHash: { type: String, required: true },
});
// A unique index on the login identifier.
accountSchema.index({ email: 1 }, { unique: true });