mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Locale parameter path traversal in pages router (#10242)
This commit is contained in:
@@ -1041,6 +1041,62 @@ describe('Pages Router', () => {
|
||||
expect(response.status).not.toBe(500);
|
||||
});
|
||||
|
||||
it('rejects locale parameter with path traversal sequences', async () => {
|
||||
const pagesDir = path.join(__dirname, 'tmp-pages-locale-test');
|
||||
const targetDir = path.join(__dirname, 'tmp-pages-locale-target');
|
||||
|
||||
try {
|
||||
await fs.mkdir(pagesDir, { recursive: true });
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Copy required HTML files to pagesDir
|
||||
const publicDir = path.resolve(__dirname, '../public');
|
||||
for (const file of ['password_reset_link_invalid.html', 'password_reset.html']) {
|
||||
const content = await fs.readFile(path.join(publicDir, file), 'utf-8');
|
||||
await fs.writeFile(path.join(pagesDir, file), content);
|
||||
}
|
||||
|
||||
// Place a probe file in target directory
|
||||
await fs.writeFile(
|
||||
path.join(targetDir, 'password_reset_link_invalid.html'),
|
||||
'<html><body>secret</body></html>'
|
||||
);
|
||||
|
||||
const traversalLocale = path.relative(pagesDir, targetDir);
|
||||
await reconfigureServer({
|
||||
...config,
|
||||
pages: {
|
||||
enableLocalization: true,
|
||||
pagesPath: pagesDir,
|
||||
},
|
||||
});
|
||||
|
||||
// Without fix: file exists at traversed path → 404 (oracle)
|
||||
// Without fix: file doesn't exist at traversed path → 200 (oracle)
|
||||
// With fix: traversal locale is rejected, always returns default page → 200
|
||||
const response = await request({
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset?token=x&locale=${encodeURIComponent(traversalLocale)}`,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
// Should serve the default page (200), not a 404 from bounds check
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// Now remove the probe file and try again — response should be the same
|
||||
await fs.rm(path.join(targetDir, 'password_reset_link_invalid.html'));
|
||||
const response2 = await request({
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset?token=x&locale=${encodeURIComponent(traversalLocale)}`,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
|
||||
// Should also be 200 — no difference reveals file existence
|
||||
expect(response2.status).toBe(200);
|
||||
} finally {
|
||||
await fs.rm(pagesDir, { recursive: true, force: true });
|
||||
await fs.rm(targetDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('does not return 500 when page parameter contains CRLF characters', async () => {
|
||||
await reconfigureServer(config);
|
||||
const crlf = 'abc\r\nX-Injected: 1';
|
||||
@@ -1444,15 +1500,17 @@ describe('Pages Router', () => {
|
||||
expect(response.text).toContain('<img');
|
||||
});
|
||||
|
||||
it('should escape XSS in locale parameter', async () => {
|
||||
it('should reject XSS payload in locale parameter', async () => {
|
||||
const xssLocale = '"><svg/onload=alert(1)>';
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/choose_password?locale=${encodeURIComponent(xssLocale)}&appId=test`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
// Invalid locale is rejected by format validation, so the XSS
|
||||
// payload never reaches the page content
|
||||
expect(response.text).not.toContain('<svg/onload=alert(1)>');
|
||||
expect(response.text).toContain('"><svg');
|
||||
expect(response.text).not.toContain('"><svg');
|
||||
});
|
||||
|
||||
it('should handle legitimate usernames with quotes correctly', async () => {
|
||||
|
||||
@@ -546,6 +546,15 @@ export class PagesRouter extends PromiseRouter {
|
||||
(req.body || {})[pageParams.locale] ||
|
||||
(req.params || {})[pageParams.locale] ||
|
||||
(req.headers || {})[pageParamHeaderPrefix + pageParams.locale];
|
||||
|
||||
// Validate locale format to prevent path traversal; only allow
|
||||
// standard locale patterns like "en", "en-US", "de-AT", "zh-Hans-CN"
|
||||
if (locale !== undefined && typeof locale !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof locale === 'string' && !/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/.test(locale)) {
|
||||
return undefined;
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user