fix: close input normalization aliases
This commit is contained in:
@@ -6,7 +6,7 @@ const normalizeAssetBase = (base: string): string => {
|
|||||||
if (
|
if (
|
||||||
!base.startsWith('/') ||
|
!base.startsWith('/') ||
|
||||||
base.startsWith('//') ||
|
base.startsWith('//') ||
|
||||||
/[\\%?#]/u.test(base) ||
|
/[\u0000-\u001f\u007f\\%?#]/u.test(base) ||
|
||||||
base.split('/').some((segment) => segment === '.' || segment === '..')
|
base.split('/').some((segment) => segment === '.' || segment === '..')
|
||||||
) {
|
) {
|
||||||
throw new Error('Asset URL base must be a safe absolute path.');
|
throw new Error('Asset URL base must be a safe absolute path.');
|
||||||
|
|||||||
@@ -208,8 +208,7 @@ const siteConfigSchema = z.object({
|
|||||||
canonicalUrl: httpUrl,
|
canonicalUrl: httpUrl,
|
||||||
buildId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/, 'Build ID must be a safe immutable identifier.'),
|
buildId: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/, 'Build ID must be a safe immutable identifier.'),
|
||||||
}).strict().superRefine((site, context) => {
|
}).strict().superRefine((site, context) => {
|
||||||
const url = new URL(site.canonicalUrl);
|
if (site.environment === 'preview' && site.canonicalUrl !== 'https://portfolio.localhost/') {
|
||||||
if (site.environment === 'preview' && (url.hostname !== 'portfolio.localhost' || url.port || url.pathname !== '/')) {
|
|
||||||
context.addIssue({ code: 'custom', message: 'Preview canonical URL must be exactly https://portfolio.localhost/.', path: ['canonicalUrl'] });
|
context.addIssue({ code: 'custom', message: 'Preview canonical URL must be exactly https://portfolio.localhost/.', path: ['canonicalUrl'] });
|
||||||
}
|
}
|
||||||
if (site.environment === 'production') {
|
if (site.environment === 'production') {
|
||||||
|
|||||||
@@ -60,6 +60,17 @@ describe('content schemas', () => {
|
|||||||
})).toThrow(/production hostname is not approved/i);
|
})).toThrow(/production hostname is not approved/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects preview URL normalization aliases instead of accepting their normalized value', () => {
|
||||||
|
for (const canonicalUrl of [
|
||||||
|
'https://portfolio.localhost/.',
|
||||||
|
'https://portfolio.localhost/%2e',
|
||||||
|
'https://portfolio.localhost/?',
|
||||||
|
]) {
|
||||||
|
expect(() => parseSiteConfig({ environment: 'preview', canonicalUrl, buildId: 'preview-1' }))
|
||||||
|
.toThrow(/exactly https:\/\/portfolio\.localhost\//i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects controls, non-NFC URLs, and ports at all public URL boundaries', () => {
|
it('rejects controls, non-NFC URLs, and ports at all public URL boundaries', () => {
|
||||||
expect(() => parseSiteConfig({
|
expect(() => parseSiteConfig({
|
||||||
environment: 'preview', canonicalUrl: 'https://portfolio.localhost/\n', buildId: 'preview-1',
|
environment: 'preview', canonicalUrl: 'https://portfolio.localhost/\n', buildId: 'preview-1',
|
||||||
|
|||||||
@@ -80,4 +80,17 @@ describe('production inputs', () => {
|
|||||||
expect(() => assetUrl(manifest, 'selected-build-one', base)).toThrow(/asset url base/i);
|
expect(() => assetUrl(manifest, 'selected-build-one', base)).toThrow(/asset url base/i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects raw C0 controls and DEL in an asset URL base before URL parsing', () => {
|
||||||
|
const manifest = parsePublicAssetManifest(JSON.parse(readFileSync(fixtureUrl('asset-manifest.valid.json'), 'utf8')));
|
||||||
|
for (const base of [
|
||||||
|
'/assets/\n../private/',
|
||||||
|
'/assets/\nprivate/',
|
||||||
|
'/assets/\tprivate/',
|
||||||
|
'/assets/\rprivate/',
|
||||||
|
'/assets/\u007fprivate/',
|
||||||
|
]) {
|
||||||
|
expect(() => assetUrl(manifest, 'selected-build-one', base)).toThrow(/asset url base/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user