From 10d759fade0a97ad7623ab46f6b4e49ffcbfa642 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 10 Jun 2026 23:05:46 +0800 Subject: [PATCH] fix(security): use CSPRNG for CSP nonce generation The CSP nonce was generated with `nanoid/non-secure`, which derives every character from `Math.random()`. A predictable nonce weakens nonce-based CSP as a defense against XSS. Switch to the crypto-backed `nanoid` entry point so the nonce stays unpredictable. --- plugins/security/src/app/extend/context.ts | 2 +- plugins/security/test/csp.test.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/plugins/security/src/app/extend/context.ts b/plugins/security/src/app/extend/context.ts index 3d9c413576..dd8fe3bc14 100644 --- a/plugins/security/src/app/extend/context.ts +++ b/plugins/security/src/app/extend/context.ts @@ -2,7 +2,7 @@ import { debuglog } from 'node:util'; import Tokens from 'csrf'; import { Context } from 'egg'; -import { nanoid } from 'nanoid/non-secure'; +import { nanoid } from 'nanoid'; import type { SecurityConfig } from '../../config/config.default.ts'; import type { HttpClientRequestURL, HttpClientOptions, HttpClientResponse } from '../../lib/extend/safe_curl.ts'; diff --git a/plugins/security/test/csp.test.ts b/plugins/security/test/csp.test.ts index 380a2a3f60..f0d7efc28a 100644 --- a/plugins/security/test/csp.test.ts +++ b/plugins/security/test/csp.test.ts @@ -94,6 +94,26 @@ describe.skipIf(process.platform === 'win32')('test/csp.test.ts', () => { const nonce = res.text; expect(res.headers['x-csp-nonce']).toBe(nonce); }); + + it('should generate unpredictable nonce even when Math.random is fixed', async () => { + // The CSP nonce must come from a cryptographically secure RNG. + // `nanoid/non-secure` derives every char from `Math.random()`, so pinning + // `Math.random` to a constant would make the nonce fully predictable. + // A CSPRNG-backed nonce stays random regardless of `Math.random`. + mm(Math, 'random', () => 0); + try { + const res1 = await app.httpRequest().get('/testcsp').expect(200); + const res2 = await app.httpRequest().get('/testcsp').expect(200); + expect(res1.text.length).toBe(16); + expect(res2.text.length).toBe(16); + // not constant across requests + expect(res1.text).not.toBe(res2.text); + // not the predictable value Math.random()===0 would produce + expect(res1.text).not.toBe('u'.repeat(16)); + } finally { + mm.restore(); + } + }); }); it('should ignore path', async () => {