mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-19 20:52:32 -06:00
fix(server): strip totp_secret_enc/totp_last_step from login responses (#100)
Review caught the encrypted TOTP secret riding in the login/verify response body: issueSession receives a SELECT * user row and only destructured out password_hash, so totp_secret_enc (and the internal replay counter totp_last_step) leaked. Encrypted, so not catastrophic, but it regresses the API work's "secrets never in responses" rule. Strip both in issueSession (covers /login and /totp/verify); add an assertion that a verify response carries no totp_secret_enc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
728f03beba
commit
f4c5865013
|
|
@ -165,7 +165,10 @@ function issueSession(req, res, user, extra = {}) {
|
|||
logSuccessfulLogin(user.id, user.email, getClientIp(req));
|
||||
const workspaceId = ensureDefaultOrgForUser(user, { allowCreate: config.autoCreateOrgOnSignup });
|
||||
const token = generateToken(user, workspaceId);
|
||||
const { password_hash, ...safeUser } = user;
|
||||
// #100: callers pass a SELECT * row. Strip password_hash AND the TOTP internals
|
||||
// (the encrypted secret + the replay counter) so no secret/internal rides in the
|
||||
// response body - "secrets never in responses", same as the API token work.
|
||||
const { password_hash, totp_secret_enc, totp_last_step, ...safeUser } = user;
|
||||
res.json({ token, user: safeUser, current_workspace_id: workspaceId, ...extra });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ test('/totp/verify completes login via recovery code; single-use; surfaces remai
|
|||
assert.ok(v1.body.token, 'recovery code yields a full session token');
|
||||
assert.equal(v1.body.via_recovery, true);
|
||||
assert.equal(v1.body.recovery_codes_remaining, 9, 'one code consumed');
|
||||
// "secrets never in responses": the encrypted TOTP secret + replay counter must not leak
|
||||
assert.ok(!JSON.stringify(v1.body).includes('totp_secret_enc'), 'no encrypted TOTP secret in the response body');
|
||||
assert.equal(v1.body.user.totp_secret_enc, undefined, 'user object carries no totp_secret_enc');
|
||||
assert.equal(v1.body.user.totp_last_step, undefined, 'user object carries no totp_last_step');
|
||||
assert.equal((await jfetch('/api/auth/me', auth(v1.body.token))).status, 200, 'full token works');
|
||||
// reuse the SAME recovery code -> rejected (single-use)
|
||||
const l2 = await jfetch('/api/auth/login', post(null, { email: u.email, password: PW }));
|
||||
|
|
|
|||
Loading…
Reference in a new issue