const { isDisposableEmail } = require('fakeout'); const LIMITS = { name: 100, username: 50, email: 254, password: 128, }; const MIN_PASSWORD_LENGTH = 12; const COMMON_PASSWORDS = new Set([ '123456789', 'password', 'password123', 'qwerty', 'qwertyuiop', 'letmein', 'welcome', 'admin', 'iloveyou', 'monkey', 'dragon', 'abc123', ]); function cleanText(value, { trim = true } = {}) { if (typeof value !== 'string') { return ''; } const cleaned = value.replace(/[\u0000-\u001F\u007F]/g, ''); return trim ? cleaned.trim() : cleaned; } function cleanPersonInput(input) { return { name: cleanText(input.name), username: cleanText(input.username), email: cleanText(input.email).toLowerCase(), // Password whitespace is preserved, but control characters are removed. password: cleanText(input.password, { trim: false }), }; } function validateCommonFields(values, { requireName = true } = {}) { const fields = requireName ? ['name', 'username', 'email', 'password'] : ['email', 'password']; for (const field of fields) { if (!values[field] || values[field].length > LIMITS[field]) { throw new Error(`${field} must be between 1 and ${LIMITS[field]} characters`); } } if (/\s/.test(values.username)) { throw new Error('username must not contain spaces'); } if (/\s/.test(values.email)) { throw new Error('email must not contain spaces'); } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { throw new Error('email must be a valid email address'); } } function validateRegistrationInput(values) { validateCommonFields(values); if (values.password.length < MIN_PASSWORD_LENGTH) { throw new Error(`password must be at least ${MIN_PASSWORD_LENGTH} characters`); } const passwordLower = values.password.toLowerCase(); const normalizedPassword = passwordLower.replace(/[^a-z0-9]/g, ''); const emailLocalPart = values.email.split('@')[0]; if ( COMMON_PASSWORDS.has(normalizedPassword) || passwordLower.includes(values.username.toLowerCase()) || passwordLower.includes(emailLocalPart) ) { throw new Error('password is too common or contains account details'); } if (!/[a-z]/.test(values.password) || !/[A-Z]/.test(values.password)) { throw new Error('password must contain lowercase and uppercase letters'); } if (!/[0-9]/.test(values.password)) { throw new Error('password must contain a number'); } if (!/[^a-zA-Z0-9\s]/.test(values.password)) { throw new Error('password must contain a special character'); } if (/(.)\1\1/.test(values.password) || /(0123|1234|2345|3456|4567|5678|6789|abcd|bcde|cdef)/i.test(values.password)) { throw new Error('password must not contain obvious repeated or sequential characters'); } if (isDisposableEmail(values.email)) { throw new Error('temporary email addresses are not allowed'); } } function sanitizePersonInput(req, res, next) { try { const values = cleanPersonInput(req.body || {}); validateRegistrationInput(values); req.person = values; next(); } catch (err) { res.status(400).send(err.message); } } function sanitizeLoginInput(req, res, next) { try { const values = cleanPersonInput(req.body || {}); validateCommonFields(values, { requireName: false }); req.loginInput = { email: values.email, password: values.password }; next(); } catch (err) { res.status(400).send(err.message); } } module.exports = { sanitizePersonInput, sanitizeLoginInput };