fix(security): stop reflecting submitted values in validation errors everywhere, cap credential lengths, close the login timing oracle

safeValidationErrors moves to utils/routeHelpers and replaces every
res.status(400).json({ errors: errors.array() }) in the routes, so no 400
body carries the submitted value any more (setup, customer auth and
customer change-password were still echoing rejected passwords).

Admin login, gallery verify, customer login/register/reset, customer
change-password and setup now cap username/slug at 255 and passwords at
MAX_PASSWORD_LENGTH at the validator, so an oversized value never reaches
the lockout lookup, bcrypt or the failed-attempt log.

Admin and customer login run one bcrypt compare on every path; the unknown
account branch used to return in microseconds against ~100ms for a wrong
password, which enumerated usernames despite the generic message.
This commit is contained in:
Paul Nothaft
2026-09-03 10:53:30 +02:00
parent 839bf4e464
commit 40a8a9882a
25 changed files with 131 additions and 97 deletions
@@ -65,8 +65,10 @@ describe('password validation length cap (zxcvbn DoS)', () => {
// No route may hand errors.array() straight to the response.
expect(src).not.toMatch(/errors:\s*errors\.array\(\)/);
// ...and the helper that replaces it must drop `value`.
expect(src).toMatch(/safeValidationErrors\s*=\s*\(errors\)\s*=>\s*errors\.array\(\)\.map\(\(\{ value, \.\.\.rest \}\)/);
// ...and the shared helper that replaces it must drop `value`.
const helper = require('fs').readFileSync(
require('path').join(__dirname, '../../src/utils/routeHelpers.js'), 'utf8');
expect(helper).toMatch(/safeValidationErrors\s*=\s*\(errors\)\s*=>\s*errors\.array\(\)\.map\(\(\{ value, \.\.\.rest \}\)/);
});
it('applies the cap through the context wrapper too', async () => {