fix(javascript): resolve query-only HTML references to their document

This commit is contained in:
morluto
2026-10-10 03:11:08 +08:00
parent 08daf056cc
commit 55a46f7c71
3 changed files with 144 additions and 5 deletions
@@ -270,7 +270,9 @@ const htmlCandidate = (
]);
if (declaredPath.startsWith("/")) return declaredPath.slice(1);
if (base === undefined || base === null || base === "")
return posix.join(posix.dirname(input.sourcePath), declaredPath);
return declaredPath === ""
? input.sourcePath
: posix.join(posix.dirname(input.sourcePath), declaredPath);
// A local base href is a second untrusted path input; apply the same
// admission rules a declared path gets so it cannot smuggle traversal or
// separator syntax past canonicalization.
@@ -286,23 +288,39 @@ const htmlCandidate = (
const basePath = decodedBase.startsWith("/")
? decodedBase.slice(1)
: posix.join(posix.dirname(input.sourcePath), decodedBase);
if (declaredPath === "")
return emptyHtmlReference(input, decodedBase, basePath);
return posix.join(htmlBaseDirectory(decodedBase, basePath), declaredPath);
};
const emptyHtmlReference = (
input: ResolveArtifactPathInput,
base: string,
basePath: string,
): string | ArtifactPathResolution => {
if (!htmlBaseIsDirectory(base)) return basePath;
const confined = confineCandidate(input, basePath);
if (typeof confined !== "string") return confined;
return unresolvedOutcome(input, "not-found", [
`The HTML reference resolves to directory ${confined || "."}, not an exact selected application file.`,
]);
};
/**
* The directory a relative HTML reference resolves against. A base path
* ending in "/" or in a "." or ".." segment is already a directory, because a
* relative reference resolves against the base URL's directory and those
* segments are dropped rather than stepped through.
*/
const htmlBaseDirectory = (base: string, basePath: string): string =>
const htmlBaseIsDirectory = (base: string): boolean =>
base.endsWith("/") ||
base.endsWith("/.") ||
base.endsWith("/..") ||
base === "." ||
base === ".."
? basePath
: posix.dirname(basePath);
base === "..";
const htmlBaseDirectory = (base: string, basePath: string): string =>
htmlBaseIsDirectory(base) ? basePath : posix.dirname(basePath);
const confineCandidate = (
input: ResolveArtifactPathInput,