fix: prevent session forgery via predictable signing key (CVE-2026-61500)

Massimo Melina committed Jul 10, 2026 at 20:29 UTC 59472e534bf7e056d708382d02935c2eaf956927
3 files changed +7 -4
src/api.auth.ts
+3 -1
@@ -15,6 +15,7 @@ import { clearTextLogin, getCurrentUsername, setLoggedIn, srpServerStep1 } from
15 import { defineConfig } from './config'
16 import events from './events'
17 import { apiAssertTypes } from './misc'
18 +import { randomUUID } from 'node:crypto'
19
20 const ongoingLogins:Record<string,SRPServerSessionStep1> = {} // store data that doesn't fit session object
21 const keepSessionAlive = defineConfig('keep_session_alive', true)
@@ -75,7 +76,8 @@ export const authApis = {
76 return new ApiError(HTTP_UNAUTHORIZED)
77 try {
78 const { srpServer, ...rest } = await srpServerStep1(account)
78 - const sid = Math.random()
79 + // keep the public handshake identifier independent of predictable application PRNG state
80 + const sid = randomUUID()
81 ongoingLogins[sid] = srpServer
82 setTimeout(()=> delete ongoingLogins[sid], 60_000)
83 ctx.session.loggingIn = { username, sid } // temporarily store until process is complete
src/cross.ts
+1 -1
@@ -330,7 +330,7 @@ export function getOrSet<T>(o: Record<string,T> | Map<string, T>, k:string, crea
330 : (o[k] = creator())
331 }
332
333 -// 10 chars is 51+bits, 8 is 41+bits
333 +// non-cryptographic; 10 chars is 51+bits, 8 is 41+bits
334 export function randomId(len = 10): string {
335 if (len > 10)
336 return randomId(10) + randomId(len - 10)
src/index.ts
+3 -2
@@ -19,7 +19,8 @@ import { adminApis } from './adminApis'
19 import { defineConfig, Version } from './config'
20 import { ok } from 'assert'
21 import _ from 'lodash'
22 -import { httpStream, randomId } from './misc'
22 +import { httpStream } from './misc'
23 +import { randomBytes } from 'node:crypto'
24 import { selfCheckMiddleware } from './selfCheck'
25 import { acmeMiddleware } from './acme'
26 import './geo'
@@ -39,7 +40,7 @@ if (new Version(process.versions.node).olderThan('18.15.0')) {
40 process.title = 'HFS ' + VERSION
41 httpStream.defaultUA = 'HFS'
42 const keys = process.env.COOKIE_SIGN_KEYS?.split(',')
42 - || [randomId(30)] // randomness at start gives some extra security, btu also invalidates existing sessions
43 + || [randomBytes(32).toString('base64url')] // cookie signatures need randomness that cannot be reconstructed from observable Math.random() output
44 export const app = new Koa({ keys })
45 app.use(sessionMiddleware)
46 .use(selfCheckMiddleware)