54 lines
1.6 KiB
TypeScript
54 lines
1.6 KiB
TypeScript
/**
|
|
* Small, dependency-free security helpers shared across packages.
|
|
*/
|
|
|
|
const HTML_ESCAPES: Record<string, string> = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
|
|
/**
|
|
* Escape a string for safe interpolation into HTML text or attributes.
|
|
* Used for page metadata (title/description) so untrusted values can't
|
|
* break out of an attribute or inject markup.
|
|
*/
|
|
export function escapeHtml(value: string): string {
|
|
return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]!);
|
|
}
|
|
|
|
/**
|
|
* Client island names come from `data-client="..."` attributes and from
|
|
* filenames in `app/client`. We only ever allow a conservative charset so a
|
|
* name can never be used to traverse the filesystem or inject code.
|
|
*/
|
|
const SAFE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
|
|
export function isSafeIslandName(name: string): boolean {
|
|
return SAFE_NAME.test(name);
|
|
}
|
|
|
|
/**
|
|
* Reject obvious path-traversal in a request path before it is ever used to
|
|
* resolve a file. The router never builds file paths from request input
|
|
* (routes are resolved against a pre-scanned table), but this is a cheap
|
|
* defense-in-depth guard.
|
|
*/
|
|
export function isSafeRequestPath(pathname: string): boolean {
|
|
if (pathname.includes("\0")) return false;
|
|
// Reject `..` segments and backslashes that could escape a directory.
|
|
const decoded = safeDecode(pathname);
|
|
if (decoded === null) return false;
|
|
return !/(^|\/)\.\.(\/|$)/.test(decoded) && !decoded.includes("\\");
|
|
}
|
|
|
|
function safeDecode(value: string): string | null {
|
|
try {
|
|
return decodeURIComponent(value);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|