Files
EpicNext-Cms/src/features/housekeeping/foundation/housekeeping-href.ts
T

70 lines
1.6 KiB
TypeScript

const HOUSEKEEPING_ORIGIN = "https://housekeeping.invalid";
function containsControlCharacter(value: string): boolean {
return Array.from(value).some((character) => {
const code = character.codePointAt(0) ?? 0;
return code < 32 || code === 127;
});
}
function decodedVariants(value: string): readonly string[] | null {
const variants = [value];
for (let pass = 0; pass < 3; pass += 1) {
let decoded: string;
try {
decoded = decodeURIComponent(variants.at(-1) ?? "");
} catch {
return null;
}
if (decoded === variants.at(-1)) break;
variants.push(decoded);
}
return variants;
}
function containsDotSegment(path: string): boolean {
return path.split("/").some((segment) => segment === "." || segment === "..");
}
export function isSafeHousekeepingHref(href: string): boolean {
if (
href.length === 0 ||
!href.startsWith("/") ||
href.startsWith("//") ||
containsControlCharacter(href) ||
href.includes("\\")
) {
return false;
}
const variants = decodedVariants(href);
if (
variants === null ||
variants.some(
(value) => containsControlCharacter(value) || value.includes("\\"),
)
) {
return false;
}
const rawPath = href.split(/[?#]/u, 1)[0] ?? "";
const pathVariants = decodedVariants(rawPath);
if (
pathVariants === null ||
pathVariants.some((path) => containsDotSegment(path))
) {
return false;
}
let normalized: URL;
try {
normalized = new URL(href, HOUSEKEEPING_ORIGIN);
} catch {
return false;
}
return (
normalized.origin === HOUSEKEEPING_ORIGIN &&
(normalized.pathname === "/ase" || normalized.pathname.startsWith("/ase/"))
);
}