Skip to content

Commit 01fb6a9

Browse files
authored
fix: Locale parameter path traversal in pages router (#10242)
1 parent cd0ec81 commit 01fb6a9

File tree

2 files changed

+69
-2
lines changed

2 files changed

+69
-2
lines changed

spec/PagesRouter.spec.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,62 @@ describe('Pages Router', () => {
10411041
expect(response.status).not.toBe(500);
10421042
});
10431043

1044+
it('rejects locale parameter with path traversal sequences', async () => {
1045+
const pagesDir = path.join(__dirname, 'tmp-pages-locale-test');
1046+
const targetDir = path.join(__dirname, 'tmp-pages-locale-target');
1047+
1048+
try {
1049+
await fs.mkdir(pagesDir, { recursive: true });
1050+
await fs.mkdir(targetDir, { recursive: true });
1051+
1052+
// Copy required HTML files to pagesDir
1053+
const publicDir = path.resolve(__dirname, '../public');
1054+
for (const file of ['password_reset_link_invalid.html', 'password_reset.html']) {
1055+
const content = await fs.readFile(path.join(publicDir, file), 'utf-8');
1056+
await fs.writeFile(path.join(pagesDir, file), content);
1057+
}
1058+
1059+
// Place a probe file in target directory
1060+
await fs.writeFile(
1061+
path.join(targetDir, 'password_reset_link_invalid.html'),
1062+
'<html><body>secret</body></html>'
1063+
);
1064+
1065+
const traversalLocale = path.relative(pagesDir, targetDir);
1066+
await reconfigureServer({
1067+
...config,
1068+
pages: {
1069+
enableLocalization: true,
1070+
pagesPath: pagesDir,
1071+
},
1072+
});
1073+
1074+
// Without fix: file exists at traversed path → 404 (oracle)
1075+
// Without fix: file doesn't exist at traversed path → 200 (oracle)
1076+
// With fix: traversal locale is rejected, always returns default page → 200
1077+
const response = await request({
1078+
url: `${config.publicServerURL}/apps/test/request_password_reset?token=x&locale=${encodeURIComponent(traversalLocale)}`,
1079+
followRedirects: false,
1080+
}).catch(e => e);
1081+
1082+
// Should serve the default page (200), not a 404 from bounds check
1083+
expect(response.status).toBe(200);
1084+
1085+
// Now remove the probe file and try again — response should be the same
1086+
await fs.rm(path.join(targetDir, 'password_reset_link_invalid.html'));
1087+
const response2 = await request({
1088+
url: `${config.publicServerURL}/apps/test/request_password_reset?token=x&locale=${encodeURIComponent(traversalLocale)}`,
1089+
followRedirects: false,
1090+
}).catch(e => e);
1091+
1092+
// Should also be 200 — no difference reveals file existence
1093+
expect(response2.status).toBe(200);
1094+
} finally {
1095+
await fs.rm(pagesDir, { recursive: true, force: true });
1096+
await fs.rm(targetDir, { recursive: true, force: true });
1097+
}
1098+
});
1099+
10441100
it('does not return 500 when page parameter contains CRLF characters', async () => {
10451101
await reconfigureServer(config);
10461102
const crlf = 'abc\r\nX-Injected: 1';
@@ -1444,15 +1500,17 @@ describe('Pages Router', () => {
14441500
expect(response.text).toContain('&lt;img');
14451501
});
14461502

1447-
it('should escape XSS in locale parameter', async () => {
1503+
it('should reject XSS payload in locale parameter', async () => {
14481504
const xssLocale = '"><svg/onload=alert(1)>';
14491505
const response = await request({
14501506
url: `http://localhost:8378/1/apps/choose_password?locale=${encodeURIComponent(xssLocale)}&appId=test`,
14511507
});
14521508

14531509
expect(response.status).toBe(200);
1510+
// Invalid locale is rejected by format validation, so the XSS
1511+
// payload never reaches the page content
14541512
expect(response.text).not.toContain('<svg/onload=alert(1)>');
1455-
expect(response.text).toContain('&quot;&gt;&lt;svg');
1513+
expect(response.text).not.toContain('&quot;&gt;&lt;svg');
14561514
});
14571515

14581516
it('should handle legitimate usernames with quotes correctly', async () => {

src/Routers/PagesRouter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,15 @@ export class PagesRouter extends PromiseRouter {
546546
(req.body || {})[pageParams.locale] ||
547547
(req.params || {})[pageParams.locale] ||
548548
(req.headers || {})[pageParamHeaderPrefix + pageParams.locale];
549+
550+
// Validate locale format to prevent path traversal; only allow
551+
// standard locale patterns like "en", "en-US", "de-AT", "zh-Hans-CN"
552+
if (locale !== undefined && typeof locale !== 'string') {
553+
return undefined;
554+
}
555+
if (typeof locale === 'string' && !/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(locale)) {
556+
return undefined;
557+
}
549558
return locale;
550559
}
551560

0 commit comments

Comments
 (0)