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)