Security & hardening pass: scrypt password hashing, persisted admin sessions, upvote/analytics rate limits, dependency fixes, healthcheck, lint cleanup; v2.10.0
Publish Container / build (push) Successful in 17m3s
Publish Container / build (push) Successful in 17m3s
- Study passwords: salted scrypt; legacy SHA-256 hashes verified and upgraded on login - Admin sessions persisted (hashed tokens) so deploys no longer log the admin out - Upvotes: rate limit + one per client per question, approved questions only; rate limit on analytics play/event - npm audit: 46 -> 4 vulnerabilities (tiptap bumped to 3.31.4) - Untrack per-user data (study users/notes/progress/etc.) from git - Dockerfile HEALTHCHECK; node --test suite for password hashing - Fix email template cards remounting (lost input focus) on each keystroke - Lint: 34 errors -> 0 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2150e1cd7
commit
8a796964b9
27 files changed
+880
-1034
No files matched your search
@@ -20,6 +20,8 @@ import {
|
||||
normalizeStudyUsername,
|
||||
isValidStudyUsername,
|
||||
hashStudyPassword,
|
||||
verifyStudyPassword,
|
||||
studyPasswordNeedsRehash,
|
||||
hashPasswordResetToken,
|
||||
hashEmailVerifyToken,
|
||||
findStudyUserByUsername,
|
||||
@@ -46,6 +48,8 @@ import {
|
||||
getCanonicalBaseUrl,
|
||||
} from '../email.js'
|
||||
|
||||
const DUMMY_PASSWORD_HASH = hashStudyPassword('timing-equalizer')
|
||||
|
||||
const studyAuthRateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 20,
|
||||
@@ -164,18 +168,19 @@ export function register(app) {
|
||||
const user = findStudyUserByUsername(username)
|
||||
|
||||
if (!user) {
|
||||
verifyStudyPassword(password, DUMMY_PASSWORD_HASH) // equalize timing with the real-user path
|
||||
res.status(401).json({ message: 'Invalid email or password.' })
|
||||
return
|
||||
}
|
||||
|
||||
const submittedHash = hashStudyPassword(password)
|
||||
const expectedHash = user.passwordHash
|
||||
const a = Buffer.from(submittedHash, 'utf8')
|
||||
const b = Buffer.from(expectedHash, 'utf8')
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
if (!verifyStudyPassword(password, user.passwordHash)) {
|
||||
res.status(401).json({ message: 'Invalid email or password.' })
|
||||
return
|
||||
}
|
||||
if (studyPasswordNeedsRehash(user.passwordHash)) {
|
||||
user.passwordHash = hashStudyPassword(password)
|
||||
queueStudyUsersWrite()
|
||||
}
|
||||
|
||||
const twoFaMethod = user.twoFaMethod ?? (user.totpSecret && user.totpVerified ? 'app' : null)
|
||||
if (twoFaMethod === 'app' && user.totpSecret && user.totpVerified) {
|
||||
@@ -342,10 +347,7 @@ export function register(app) {
|
||||
app.post('/api/study-auth/totp-disable', studyAuthRateLimiter, requireStudyAuth, (req, res) => {
|
||||
const user = req.studyUser
|
||||
const { password } = req.body ?? {}
|
||||
const submittedHash = hashStudyPassword(typeof password === 'string' ? password : '')
|
||||
const a = Buffer.from(submittedHash, 'utf8')
|
||||
const b = Buffer.from(user.passwordHash, 'utf8')
|
||||
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
||||
if (!verifyStudyPassword(typeof password === 'string' ? password : '', user.passwordHash)) {
|
||||
res.status(401).json({ message: 'Incorrect password.' })
|
||||
return
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user