@samitouri / QOSami-HFS / commits / bf5a9624

fix: wrong allow_net cache across accounts

Massimo Melina committed Jul 18, 2026 at 23:11 UTC bf5a96248c20b50a1e5b53c1bddd287ea49325c3
3 files changed +29 -4
src/auth.ts
+4 -1
@@ -65,7 +65,10 @@ export async function setLoggedIn(ctx: Context, username: string | false) {
65 const a = ctx.state.account = getAccount(username)
66 if (!a) return
67 await events.emitAsync('finalizingLogin', { ctx, username, inputs: { ...ctx.state.params, ...ctx.query } })
68 - s.username = normalizeUsername(username)
68 + const normalized = normalizeUsername(username)
69 + if (s.username !== normalized)
70 + delete s.allowNet // discard restrictions cached for another identity before replacing the session account
71 + s.username = normalized
72 s.ts = Date.now()
73 const k = ALLOW_SESSION_IP_CHANGE
74 s[k] = k in ctx.query || Boolean(ctx.state.params?.[k]) || undefined // login APIs will get ctx.state.params, others can rely on ctx.query
src/middlewares.ts
+5 -3
@@ -6,7 +6,7 @@ import { API_URI, DEV } from './const'
6 import { ALLOW_SESSION_IP_CHANGE, DAY, hasDirTraversal, isLocalHost, netMatches, splitAt, stream2string, try_, tryJson } from './misc'
7 import { Readable } from 'stream'
8 import { applyBlock } from './block'
9 -import { Account, accountCanLogin, accounts, getAccount, getFromAccount } from './perm'
9 +import { Account, accountCanLogin, accounts, getAccount, getFromAccount, normalizeUsername } from './perm'
10 import { Connection, normalizeIp, socket2connection, updateConnectionForCtx } from './connections'
11 import { clearTextLogin, invalidateSessionBefore, setLoggedIn } from './auth'
12 import { constants } from 'zlib'
@@ -145,9 +145,11 @@ export const prepareState: Koa.Middleware = async (ctx, next) => {
145 }
146
147 export function failAllowNet(ctx: Koa.Context, a: Account | undefined) {
148 - const cached = ctx.session?.allowNet // won't reflect changes until session is terminated
148 + // a cached mask is valid only for the identity that stored it
149 + const sameAccount = ctx.session?.username === normalizeUsername(a?.username || '')
150 + const cached = sameAccount ? ctx.session?.allowNet : undefined
151 const mask = cached ?? getFromAccount(a || '', a => a.allow_net)
150 - if (!cached && mask && ctx.session?.username)
152 + if (sameAccount && !cached && mask)
153 ctx.session.allowNet = mask // must be deleted on logout by setLoggedIn
154 const ret = mask && !netMatches(ctx.ip, mask, true)
155 if (ret)
tests/test.ts
+20
@@ -878,6 +878,26 @@ describe('sessions', () => {
878 await reqApi('del_account', { username: user }, 200, adminReq)().catch(() => {})
879 }
880 })
881 + test('allow_net cache follows account switch', async () => {
882 + const u = `allow-net-switch-${randomId(6)}`.toLowerCase()
883 + const p = `pw-${randomId(8)}`
884 + const adminReq = { auth, jar: {} }
885 + try {
886 + await reqApi('add_account', { username: u, password: p, allow_net: '192.0.2.1' },
887 + res => res?.username === u, adminReq)()
888 + const jar = {}
889 + // cache the current account mask before presenting credentials for another account
890 + await reqApi('refresh_session', {}, res => res?.username === username, { auth, jar })()
891 + await reqApi('refresh_session', {}, res => res?.username === username, { jar })()
892 + await reqApi('refresh_session', {}, res => {
893 + if (res?.username)
894 + throw Error(`account switch bypassed allow_net as ${res.username}`)
895 + }, { auth: `${u}:${p}`, jar })()
896 + }
897 + finally {
898 + await reqApi('del_account', { username: u }, 200, adminReq)().catch(() => {})
899 + }
900 + })
901 test('auto_login_net.canLogin', async () => {
902 const user = `auto-login-${randomId(6)}`.toLowerCase()
903 const adminReq = { auth, jar: {} }