diff --git a/src/core/AssetUrls.ts b/src/core/AssetUrls.ts index 951cf325e..9a641391e 100644 --- a/src/core/AssetUrls.ts +++ b/src/core/AssetUrls.ts @@ -8,6 +8,19 @@ function safeDecodeAssetSegment(segment: string): string { } } +function assertSafeAssetSegment(segment: string): string { + const decodedSegment = safeDecodeAssetSegment(segment); + if ( + segment === "." || + segment === ".." || + decodedSegment === "." || + decodedSegment === ".." + ) { + throw new Error(`Invalid asset path segment: ${segment}`); + } + return decodedSegment; +} + export function encodeAssetPath(path: string): string { return normalizeAssetPath(path) .split("/") @@ -21,7 +34,7 @@ export function normalizeAssetPath(path: string): string { .replace(/^\/+/, "") .split("/") .filter((segment) => segment.length > 0) - .map((segment) => safeDecodeAssetSegment(segment)) + .map((segment) => assertSafeAssetSegment(segment)) .join("/"); } diff --git a/tests/AssetUrls.test.ts b/tests/AssetUrls.test.ts index 70f5bc581..934e629e2 100644 --- a/tests/AssetUrls.test.ts +++ b/tests/AssetUrls.test.ts @@ -27,4 +27,13 @@ describe("AssetUrls", () => { test("falls back to the unversioned path when manifest has no match", () => { expect(buildAssetUrl("images/unknown.svg", {})).toBe("/images/unknown.svg"); }); + + test("rejects dot segments in asset paths", () => { + expect(() => buildAssetUrl("../api/instance", {})).toThrow( + "Invalid asset path segment: ..", + ); + expect(() => buildAssetUrl("images/%2e%2e/secret.svg", {})).toThrow( + "Invalid asset path segment: %2e%2e", + ); + }); });