harden api updateAccount and set_account

Massimo Melina committed Oct 28, 2025 at 10:48 UTC 673bb63b7dd049c4ea51e509e6894709f0b47bc0
2 files changed +9 -5
src/api.accounts.ts
+2 -2
@@ -62,7 +62,7 @@ export default {
62 if (!acc)
63 return new ApiError(HTTP_BAD_REQUEST)
64 await updateAccount(acc, pickProps(changes, ALLOWED_KEYS))
65 - if (changes.username && ctx.session?.username === normalizeUsername(username))
65 + if (changes.username && ctx.session?.username === normalizeUsername(username)) // update session if necessary
66 ctx.session!.username = normalizeUsername(changes.username)
67 return _.pick(acc, 'username')
68 },
@@ -91,7 +91,7 @@ export default {
91
92 invalidate_sessions({ username }) {
93 apiAssertTypes({ string: { username } })
94 - invalidateSessionBefore.set(username, Date.now())
94 + invalidateSessionBefore.set(normalizeUsername(username), Date.now())
95 return {}
96 },
97
src/perm.ts
+7 -3
@@ -9,9 +9,9 @@ import { ApiError } from './apiMiddleware'
9 import { getCurrentUsername } from './auth'
10 import Koa from 'koa'
11
12 +// for all the Account fields, falsy values must be equivalent to undefined. If this changes in the future, please adjust addAccount and setAccount
13 export interface Account {
13 - // we consider all the following fields, when falsy, as equivalent to be missing. If this changes in the future, please adjust addAccount and setAccount
14 - username: string, // we keep username property (hidden) so we don't need to pass it separately
14 + username: string, // we keep username property for convenience, but hidden as we don't persist it inside the object, but as key of the accounts map
15 password?: string
16 srp?: string
17 belongs?: string[]
@@ -84,8 +84,12 @@ export async function updateAccount(account: Account, change: Partial<Account> |
84 const { username: usernameWas } = account
85 if (typeof change === 'function')
86 await change?.(account)
87 - else
87 + else {
88 + const u = normalizeUsername(change.username || '')
89 + if (u && u !== usernameWas && getAccount(u))
90 + throw "username already exists"
91 Object.assign(account, objSameKeys(change, x => x || undefined))
92 + }
93 for (const [k,v] of typedEntries(account))
94 if (!v) delete account[k] // we consider all account fields, when falsy, as equivalent to be missing (so, default value applies)
95 const { username, password } = account