Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions packages/vinext/src/build/prerender-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ function validatePagesStaticPathsResult(

type DynamicPatternParam = { name: string; optional: boolean; repeat: boolean };

function hasUnsafeRawUrlPathCharacter(value: string): boolean {
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
if (code === 92 || code <= 31 || code === 127) return true;
}
return false;
}

function getDynamicPatternParams(pattern: string): DynamicPatternParam[] {
return pattern
.split("/")
Expand Down Expand Up @@ -184,6 +192,12 @@ function validateDiscoveredParams(
`Parameter ${name} from ${source} for ${pattern} must be ${repeat ? "an array of strings" : "a string"}.`,
);
}
const values = Array.isArray(paramValue) ? paramValue : [paramValue];
if (values.some((entry) => entry === "." || entry === "..")) {
throw new Error(
`Parameter ${name} from ${source} for ${pattern} must not contain dot path segments.`,
);
}
}
return params as Record<string, string | string[]>;
}
Expand All @@ -194,19 +208,26 @@ function validatePagesStaticPathsEntry(entry: StaticPathsEntry, pattern: string)
!entry.startsWith("/") ||
entry.includes("//") ||
entry.includes("?") ||
entry.includes("#")
entry.includes("#") ||
hasUnsafeRawUrlPathCharacter(entry)
) {
throw new Error(
`The provided path \`${entry}\` from getStaticPaths does not match the route pattern \`${pattern}\`.`,
);
}
let decodedSegments: string[];
try {
for (const segment of entry.split("/")) decodeURIComponent(segment);
decodedSegments = entry.split("/").map((segment) => decodeURIComponent(segment));
} catch {
throw new Error(
`The provided path \`${entry}\` from getStaticPaths contains malformed percent-encoding.`,
);
}
if (decodedSegments.some((segment) => segment === "." || segment === "..")) {
throw new Error(
`The provided path \`${entry}\` from getStaticPaths contains a dot path segment.`,
);
}
return entry;
}
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
Expand Down
4 changes: 4 additions & 0 deletions tests/prerender-paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,10 +856,13 @@ describe("prerender path manifest", () => {
["an extra entry key", "/posts/[slug].tsx", { extra: true, params: { slug: "x" } }],
["a numeric dynamic param", "/posts/[slug].tsx", { params: { slug: 123 } }],
["an array dynamic param", "/posts/[slug].tsx", { params: { slug: ["a", "b"] } }],
["a dot-segment dynamic param", "/posts/[slug].tsx", { params: { slug: "." } }],
["a scalar catch-all param", "/docs/[...parts].tsx", { params: { parts: "a" } }],
["a query-bearing string path", "/posts/[slug].tsx", "/posts/query?x=1"],
["a relative string path", "/posts/[slug].tsx", "posts/x"],
["a double-slash string path", "/posts/[slug].tsx", "/posts//x"],
["a raw backslash string path", "/posts/[slug].tsx", "/posts\\admin"],
["an encoded dot-segment string path", "/posts/[...slug].tsx", "/posts/%2E%2E/admin"],
["malformed percent-encoding", "/posts/[slug].tsx", "/posts/%ZZ/"],
])("fails path discovery for getStaticPaths entry with %s", async (_name, file, entry) => {
writeFile("package.json", JSON.stringify({ type: "module" }));
Expand All @@ -884,6 +887,7 @@ describe("prerender path manifest", () => {

it.each([
["a numeric dynamic param", "app/posts/[slug]/page.tsx", [{ slug: 123 }]],
["a dot-segment dynamic param", "app/posts/[slug]/page.tsx", [{ slug: ".." }]],
["a scalar catch-all param", "app/docs/[...parts]/page.tsx", [{ parts: "a" }]],
["a missing optional catch-all", "app/docs/[[...parts]]/page.tsx", [{}]],
])("fails App path discovery for generateStaticParams with %s", async (_name, file, result) => {
Expand Down
Loading