豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit e266cff

Browse files
authored
Merge commit from fork
1 parent 00d4728 commit e266cff

File tree

2 files changed

+44
-40
lines changed

2 files changed

+44
-40
lines changed

.changeset/slick-ducks-shine.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"apostrophe": patch
3+
---
4+
5+
Security: ensure a minimum 2-second delay in the password reset flow to avoid disclosing whether the email or username was valid or not. Thanks to [restriction](https://github.com/restriction) for reporting the issue and proposing the fix.

packages/apostrophe/modules/@apostrophecms/login/index.js

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -293,68 +293,67 @@ module.exports = {
293293
},
294294
...self.isPasswordResetEnabled() && {
295295
async resetRequest(req) {
296-
const wait = (t = 2000) => Promise.delay(t);
296+
const MIN_RESPONSE_TIME = 2000;
297+
const startTime = Date.now();
297298
const site = (req.headers.host || '').replace(/:\d+$/, '');
298299
const email = self.apos.launder.string(req.body.email);
299300
if (!email.length) {
300301
throw self.apos.error('invalid', req.t('apostrophe:loginResetEmailRequired'));
301302
}
302303
let user;
303-
// error not reported to browser for security reasons
304304
try {
305305
user = await self.getPasswordResetUser(req.body.email);
306306
} catch (e) {
307307
self.apos.util.error(e);
308308
}
309309
if (!user) {
310-
await wait();
311310
self.apos.util.error(
312311
`Reset password request error - the user ${email} doesn\`t exist.`
313312
);
314-
return;
315-
}
316-
if (!user.email) {
317-
await wait();
313+
} else if (!user.email) {
318314
self.apos.util.error(
319315
`Reset password request error - the user ${user.username} doesn\`t have an email.`
320316
);
321-
return;
322-
}
323-
const reset = self.apos.util.generateId();
324-
user.passwordReset = reset;
325-
user.passwordResetAt = new Date();
326-
await self.apos.user.update(req, user, { permissions: false });
327-
// Fix - missing host in the absoluteUrl results in a panic.
328-
let port = (req.headers.host || '').split(':')[1];
329-
if (!port || [ '80', '443' ].includes(port)) {
330-
port = '';
331317
} else {
332-
port = `:${port}`;
318+
const reset = self.apos.util.generateId();
319+
user.passwordReset = reset;
320+
user.passwordResetAt = new Date();
321+
await self.apos.user.update(req, user, { permissions: false });
322+
let port = (req.headers.host || '').split(':')[1];
323+
if (!port || [ '80', '443' ].includes(port)) {
324+
port = '';
325+
} else {
326+
port = `:${port}`;
327+
}
328+
const parsed = new URL(
329+
req.absoluteUrl,
330+
self.apos.baseUrl
331+
? undefined
332+
: `${req.protocol}://${req.hostname}${port}`
333+
);
334+
parsed.pathname = self.login();
335+
parsed.search = '?';
336+
parsed.searchParams.append('reset', reset);
337+
parsed.searchParams.append('email', user.email);
338+
try {
339+
await self.email(req, 'passwordResetEmail', {
340+
user,
341+
url: parsed.toString(),
342+
site
343+
}, {
344+
to: user.email,
345+
subject: req.t('apostrophe:passwordResetRequest', { site })
346+
});
347+
} catch (err) {
348+
self.apos.util.error(`Error while sending email to ${user.email}`, err);
349+
}
333350
}
334-
const parsed = new URL(
335-
req.absoluteUrl,
336-
self.apos.baseUrl
337-
? undefined
338-
: `${req.protocol}://${req.hostname}${port}`
339-
);
340-
parsed.pathname = self.login();
341-
parsed.search = '?';
342-
parsed.searchParams.append('reset', reset);
343-
parsed.searchParams.append('email', user.email);
344-
try {
345-
await self.email(req, 'passwordResetEmail', {
346-
user,
347-
url: parsed.toString(),
348-
site
349-
}, {
350-
to: user.email,
351-
subject: req.t('apostrophe:passwordResetRequest', { site })
352-
});
353-
} catch (err) {
354-
self.apos.util.error(`Error while sending email to ${user.email}`, err);
351+
// Pad all paths to a constant minimum duration
352+
const elapsed = Date.now() - startTime;
353+
if (elapsed < MIN_RESPONSE_TIME) {
354+
await Promise.delay(MIN_RESPONSE_TIME - elapsed);
355355
}
356356
},
357-
358357
async reset(req) {
359358
const password = self.apos.launder.string(req.body.password);
360359
if (!password.length) {

0 commit comments

Comments
 (0)