From fe76891de46113e7becfb72b9376dac0ad464ca7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:57:04 +0000 Subject: [PATCH 1/2] fix: Validate token type in PagesRouter to prevent type confusion errors Replace unsafe `.toString()` coercion with strict type check for token parameters in verifyEmail, resendVerificationEmail, requestResetPassword, and resetPassword handlers. Non-string tokens are now treated as undefined instead of attempting coercion which throws on crafted objects. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/PagesRouter.spec.js | 50 ++++++++++++++++++++++++++++++++++++++ src/Routers/PagesRouter.js | 8 +++--- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index e20be40bfd..f25035ec2c 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -990,6 +990,56 @@ describe('Pages Router', () => { await fs.rm(baseDir, { recursive: true, force: true }); } }); + + it('rejects non-string token in verifyEmail', async () => { + await reconfigureServer(config); + const url = `${config.publicServerURL}/apps/test/verify_email?token[toString]=abc`; + const response = await request({ + url: url, + followRedirects: false, + }).catch(e => e); + expect(response.status).not.toBe(500); + }); + + it('rejects non-string token in requestResetPassword', async () => { + await reconfigureServer(config); + const url = `${config.publicServerURL}/apps/test/request_password_reset?token[toString]=abc`; + const response = await request({ + url: url, + followRedirects: false, + }).catch(e => e); + expect(response.status).not.toBe(500); + }); + + it('rejects non-string token in resetPassword via POST', async () => { + await reconfigureServer(config); + const url = `${config.publicServerURL}/apps/test/request_password_reset`; + const response = await request({ + method: 'POST', + url: url, + headers: { + 'Content-Type': 'application/json', + }, + body: { token: { toString: 'abc' }, new_password: 'newpass123' }, + followRedirects: false, + }).catch(e => e); + expect(response.status).not.toBe(500); + }); + + it('rejects non-string token in resendVerificationEmail via POST', async () => { + await reconfigureServer(config); + const url = `${config.publicServerURL}/apps/test/resend_verification_email`; + const response = await request({ + method: 'POST', + url: url, + headers: { + 'Content-Type': 'application/json', + }, + body: { token: { toString: 'abc' } }, + followRedirects: false, + }).catch(e => e); + expect(response.status).not.toBe(500); + }); }); describe('custom route', () => { diff --git a/src/Routers/PagesRouter.js b/src/Routers/PagesRouter.js index 59c4abf631..12d4e7253c 100644 --- a/src/Routers/PagesRouter.js +++ b/src/Routers/PagesRouter.js @@ -84,7 +84,7 @@ export class PagesRouter extends PromiseRouter { verifyEmail(req) { const config = req.config; const { token: rawToken } = req.query; - const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + const token = typeof rawToken === 'string' ? rawToken : undefined; if (!config) { this.invalidRequest(); @@ -109,7 +109,7 @@ export class PagesRouter extends PromiseRouter { const config = req.config; const username = req.body?.username; const rawToken = req.body?.token; - const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + const token = typeof rawToken === 'string' ? rawToken : undefined; if (!config) { this.invalidRequest(); @@ -151,7 +151,7 @@ export class PagesRouter extends PromiseRouter { } const { token: rawToken } = req.query; - const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + const token = typeof rawToken === 'string' ? rawToken : undefined; if (!token) { return this.goToPage(req, pages.passwordResetLinkInvalid); @@ -180,7 +180,7 @@ export class PagesRouter extends PromiseRouter { } const { new_password, token: rawToken } = req.body || {}; - const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken; + const token = typeof rawToken === 'string' ? rawToken : undefined; if ((!token || !new_password) && req.xhr === false) { return this.goToPage(req, pages.passwordResetLinkInvalid); From 6ddeb678b0bca5a83f0525c939c28b35bd94bf31 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:11:13 +0000 Subject: [PATCH 2/2] fix --- spec/RegexVulnerabilities.spec.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/RegexVulnerabilities.spec.js b/spec/RegexVulnerabilities.spec.js index f22ef868a8..297be93599 100644 --- a/spec/RegexVulnerabilities.spec.js +++ b/spec/RegexVulnerabilities.spec.js @@ -345,10 +345,10 @@ describe('Regex Vulnerabilities', () => { describe('on resend verification email', () => { // The PagesRouter uses express.urlencoded({ extended: false }) which does not parse // nested objects (e.g. token[$regex]=^.), so the HTTP layer already blocks object injection. - // The toString() guard in resendVerificationEmail() is defense-in-depth in case the - // body parser configuration changes. These tests verify the guard works correctly + // Non-string tokens are rejected (treated as undefined) to prevent both NoSQL injection + // and type confusion errors. These tests verify the guard works correctly // by directly testing the PagesRouter method. - it('should sanitize non-string token to string via toString()', async () => { + it('should reject non-string token as undefined', async () => { const { PagesRouter } = require('../lib/Routers/PagesRouter'); const router = new PagesRouter(); const goToPage = spyOn(router, 'goToPage').and.returnValue(Promise.resolve()); @@ -363,10 +363,9 @@ describe('Regex Vulnerabilities', () => { }, }; await router.resendVerificationEmail(req); - // The token passed to userController.resendVerificationEmail should be a string + // Non-string token should be treated as undefined const passedToken = resendSpy.calls.first().args[2]; - expect(typeof passedToken).toEqual('string'); - expect(passedToken).toEqual('[object Object]'); + expect(passedToken).toBeUndefined(); }); it('should pass through valid string token unchanged', async () => {