-
Notifications
You must be signed in to change notification settings - Fork 11
Open
Labels
Description
redirect currently accepts a string as target. Hardened alternative:
type RedirectionStatus =
| 300 // MULTIPLE_CHOICES
| 301 // MOVED_PERMANENTLY
| 302 // FOUND
| 303 // SEE_OTHER
| 304 // NOT_MODIFIED
// 305 USE_PROXY (deprecated)
// 306 SWITCH_PROXY (reserved)
| 307 // TEMPORARY_REDIRECT
| 308 // PERMANENT_REDIRECT
;
type RedirectObject = {
// must starts with "/"
pathname: string;
query?: Record<string, boolean | null | number | string | undefined>;
};
type RedirectTarget = RedirectObject | string;
type RedirectOptions = {
// Allow absolute external redirects, default: `false` (same-origin only)
allowExternal?: boolean;
// Base URL for normalising relative paths and to compare origins, default `request.url`
base?: string | URL;
// Max allowed `Location` header size (bytes), default `2048`
maxLocationBytes?: number;
};
type RedirectInit = {
headers?: HeadersInit;
status?: RedirectionStatus; // default FOUND (302)
} & Omit<ResponseInit, "headers" | "status"> & RedirectOptions;
export default function redirect(target: RedirectTarget, init: RedirectInit = {}): ResponseFunction {
// impl
}hardening todos (see helpers):
- block CR/LF
- block protocol-relative
- strip fragments
- enforce same-origin unless disabled
- cap
Locationlength
helpers:
function hasCRLF(s: string): boolean {
return /[\r\n]/.test(s);
}
function isAbsoluteUrl(s: string): boolean {
try {
new URL(s);
return true;
} catch {
return false;
}
}
function encodePathname(s: string): string {
// Expect strict origin-form path ("/..."), disallow protocol-relative and
// backslashes
if (!s.startsWith("/") || s.startsWith("//") || s.includes("\\")) {
throw new Error("invalid pathname");
}
// Encode per segment to avoid double-encoding percent-escapes
return s.split("/")
.map((each, i) => i === 0 ? "" : encodeURIComponent(each)).join("/");
}
function toSearch(query?: RedirectObject["query"]) {
if (query === undefined) {
return "";
}
const search = Object.entries(query)
.filter(([, v]) => v !== null && v !== undefined)
.reduce((params, [k, v]) => {
params.append(k, String(v));
return params;
}, new URLSearchParams())
.toString()
;
return search ? "?" + search : "";
}
function toNormalized(relative: string, base?: string | URL): string {
// base -> URL resolution
if (base !== undefined) {
const url = new URL(relative, new URL(String(base)));
return url.pathname + url.search;
}
// no base -> enforce strict single-slash path and strip fragment
if (!relative.startsWith("/") || relative.startsWith("//")) {
throw new Error("bad relative");
}
const i = relative.indexOf("#");
return i === -1 ? relative : relative.slice(0, i);
}