first commit
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user