From 52dcbf17f36acef5e8966d59445e0ba28de859f3 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:31:49 +0000 Subject: [PATCH 1/2] test: add locale path traversal oracle tests for PagesRouter Add tests proving that path traversal via locale parameter does not create a file existence oracle when enableLocalization is enabled. --- spec/PagesRouter.spec.js | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index b9db1fb715..7a413dcd2d 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -1178,6 +1178,73 @@ describe('Pages Router', () => { expect(nonExistingResponse.status).toBe(303); expect(nonExistingResponse.headers.location).toContain('email_verification_send_fail'); }); + + it('does not create file existence oracle via path traversal in locale query parameter', async () => { + // Create a canary file at a traversable path to test the oracle + const canaryDir = path.join(__dirname, 'tmp-locale-oracle-test'); + try { + await fs.mkdir(canaryDir, { recursive: true }); + await fs.writeFile(path.join(canaryDir, 'password_reset.html'), 'canary'); + + config.pages.enableLocalization = true; + await reconfigureServer(config); + + // Calculate traversal from pages directory to canary directory + const pagesPath = path.resolve(__dirname, '../public'); + const relativePath = path.relative(pagesPath, canaryDir); + + // Request with path traversal locale pointing to existing canary file + const existsResponse = await request({ + url: `${config.publicServerURL}/apps/${config.appId}/request_password_reset?token=test&username=test&locale=${encodeURIComponent(relativePath)}`, + followRedirects: false, + }).catch(e => e); + + // Request with path traversal locale pointing to non-existing directory + const notExistsResponse = await request({ + url: `${config.publicServerURL}/apps/${config.appId}/request_password_reset?token=test&username=test&locale=${encodeURIComponent('../../../../../../tmp/nonexistent-dir')}`, + followRedirects: false, + }).catch(e => e); + + // Both responses must have the same status — no differential oracle + expect(existsResponse.status).toBe(notExistsResponse.status); + } finally { + await fs.rm(canaryDir, { recursive: true, force: true }); + } + }); + + it('does not create file existence oracle via path traversal in locale header', async () => { + // Create a canary file at a traversable path + const canaryDir = path.join(__dirname, 'tmp-locale-header-test'); + try { + await fs.mkdir(canaryDir, { recursive: true }); + await fs.writeFile(path.join(canaryDir, 'password_reset.html'), 'canary'); + + config.pages.enableLocalization = true; + await reconfigureServer(config); + + const pagesPath = path.resolve(__dirname, '../public'); + const relativePath = path.relative(pagesPath, canaryDir); + + // Request with path traversal locale via header pointing to existing file + const existsResponse = await request({ + url: `${config.publicServerURL}/apps/${config.appId}/request_password_reset?token=test&username=test`, + headers: { 'x-parse-page-param-locale': relativePath }, + followRedirects: false, + }).catch(e => e); + + // Request with path traversal locale via header pointing to non-existing directory + const notExistsResponse = await request({ + url: `${config.publicServerURL}/apps/${config.appId}/request_password_reset?token=test&username=test`, + headers: { 'x-parse-page-param-locale': '../../../../../../tmp/nonexistent-dir' }, + followRedirects: false, + }).catch(e => e); + + // Both responses must have the same status — no differential oracle + expect(existsResponse.status).toBe(notExistsResponse.status); + } finally { + await fs.rm(canaryDir, { recursive: true, force: true }); + } + }); }); describe('custom route', () => { From c83075fff943646f2c3c591869f64a3454d9e77a Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:40:05 +0000 Subject: [PATCH 2/2] test: add canary content assertions to locale oracle tests --- spec/PagesRouter.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 7a413dcd2d..12fb490106 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -1207,6 +1207,9 @@ describe('Pages Router', () => { // Both responses must have the same status — no differential oracle expect(existsResponse.status).toBe(notExistsResponse.status); + // Canary content must never be served + expect(existsResponse.text).not.toContain('canary'); + expect(notExistsResponse.text).not.toContain('canary'); } finally { await fs.rm(canaryDir, { recursive: true, force: true }); } @@ -1241,6 +1244,9 @@ describe('Pages Router', () => { // Both responses must have the same status — no differential oracle expect(existsResponse.status).toBe(notExistsResponse.status); + // Canary content must never be served + expect(existsResponse.text).not.toContain('canary'); + expect(notExistsResponse.text).not.toContain('canary'); } finally { await fs.rm(canaryDir, { recursive: true, force: true }); }