Skip to content

Commit 70f95be

Browse files
IM.codesclaude
andcommitted
security: fix remaining audit findings — refresh bypass, registration bypass, admin protection
Critical: - Refresh token rotation now checks user.status — disabled users can no longer refresh sessions (consumes token but rejects new pair) - Public registration (POST /api/auth/register) now checks registration_enabled + require_approval settings High: - Default admin (username='admin') cannot be disabled, not just deleted - User deletion cascades: revokes refresh_tokens, api_keys, deletes passkey_credentials before removing user row Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 9192c1d commit 70f95be

3 files changed

Lines changed: 28 additions & 1 deletion

File tree

server/src/db/queries.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export async function updateUserStatus(db: PgDatabase, userId: string, status: '
104104
}
105105

106106
export async function deleteUser(db: PgDatabase, userId: string): Promise<void> {
107+
// Cascade: revoke all auth artifacts before deleting user
108+
await db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').bind(userId).run();
109+
await db.prepare('UPDATE api_keys SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL').bind(Date.now(), userId).run();
110+
await db.prepare('DELETE FROM passkey_credentials WHERE user_id = ?').bind(userId).run();
107111
await db.prepare('DELETE FROM users WHERE id = ?').bind(userId).run();
108112
}
109113

server/src/routes/admin.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ adminRoutes.post('/users/:id/disable', async (c) => {
6969
const target = await getUserById(c.env.DB, targetId);
7070
if (!target) return c.json({ error: 'not_found' }, 404);
7171

72+
// Default admin cannot be disabled
73+
if (target.username === 'admin') return c.json({ error: 'cannot_disable_admin' }, 403);
74+
7275
// Cannot disable the last active admin
7376
if (target.is_admin && target.status === 'active') {
7477
const adminCount = await countActiveAdmins(c.env.DB);

server/src/routes/auth.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Hono } from 'hono';
22
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
33
import type { Env } from '../env.js';
4-
import { createUser, getUserById, getUserByUsername } from '../db/queries.js';
4+
import { createUser, getUserById, getUserByUsername, getSetting, updateUserStatus } from '../db/queries.js';
55
import { randomHex, sha256Hex, signJwt, verifyJwt, hashPassword, verifyPassword } from '../security/crypto.js';
66
import { checkIdempotency, recordIdempotency } from '../security/replay.js';
77
import { logAudit } from '../security/audit.js';
@@ -64,6 +64,12 @@ async function resolveUserId(c: AnyAuthContext): Promise<string | null> {
6464

6565
// POST /api/auth/register — create a new user and issue initial API key
6666
authRoutes.post('/register', async (c) => {
67+
// Check if registration is enabled
68+
const regEnabled = await getSetting(c.env.DB, 'registration_enabled');
69+
if (regEnabled === 'false') {
70+
return c.json({ error: 'registration_disabled' }, 403);
71+
}
72+
6773
// Idempotency: deduplicate retried registration requests
6874
const idempotencyKey = c.req.header('Idempotency-Key');
6975
if (idempotencyKey) {
@@ -74,6 +80,12 @@ authRoutes.post('/register', async (c) => {
7480
const userId = randomHex(16);
7581
await createUser(c.env.DB, userId);
7682

83+
// Set status to pending if approval is required
84+
const requireApproval = await getSetting(c.env.DB, 'require_approval');
85+
if (requireApproval === 'true') {
86+
await updateUserStatus(c.env.DB, userId, 'pending');
87+
}
88+
7789
const rawKey = `deck_${randomHex(32)}`;
7890
const keyHash = sha256Hex(rawKey);
7991
const now = Date.now();
@@ -302,6 +314,14 @@ authRoutes.post('/refresh', async (c) => {
302314
return c.json({ error: 'invalid_token' }, 401);
303315
}
304316

317+
// Reject disabled/pending users — prevents token refresh after admin disables account
318+
const refreshUser = await getUserById(c.env.DB, row.user_id);
319+
if (!refreshUser || refreshUser.status !== 'active') {
320+
// Consume the token to prevent replay, but don't issue new ones
321+
await c.env.DB.prepare('UPDATE refresh_tokens SET used_at = ? WHERE id = ?').bind(Date.now(), row.id).run();
322+
return c.json({ error: 'account_disabled' }, 403);
323+
}
324+
305325
// Per-user lockout check
306326
const userLockout = await checkAuthLockout(c.env.DB, `user:${row.user_id}`);
307327
if (userLockout.locked) {

0 commit comments

Comments
 (0)