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: {} }