Safe Url
Defensive URL parsing with a protocol allowlist (HTTPS-only by default), authority validation, IDN-homograph defense, and a length cap that runs BEFORE Node's WHATWG URL parser sees the input. The framework's stance on outbound URLs: TLS-required by default; cleartext (http: / ws:) is opt-in per call via opts.allowedProtocols. user:pass@ userinfo refuses by default — credentials belong in headers / a credential store, not in URL strings that leak into request logs, error messages, metric labels, and trace spans. Mixed-script host labels (Cyrillic 'о' inside an otherwise-Latin label, etc. — UTS #39 §5 homograph shape) refuse by default and emit safeurl.idn_homograph.refused to the audit chain so a forensic review can reconstruct every accepted host.
Pre-baked protocol allowlists are exposed as frozen arrays so each caller can declare a NARROW per-call allowlist (the http-client speaks HTTP, not WebSocket; a wss:// URL handed to it is a category error that should fail loudly here, not later inside a transport):
ALLOW_HTTP_TLS ["https:"] (secure HTTP default) ALLOW_HTTP_ALL ["http:", "https:"] (HTTP + cleartext opt-in) ALLOW_WS_TLS ["wss:"] (secure WS default) ALLOW_WS_ALL ["ws:", "wss:"] (WS + cleartext opt-in) ALLOW_ANY ["http:", "https:", "ws:", "wss:"]
parse throws SafeUrlError (or a caller-supplied error class via opts.errorClass, used by b.objectStore / b.logStream / b.httpClient to surface their own decorated error type) with a stable .code: safe-url/missing / safe-url/too-long / safe-url/malformed / safe-url/protocol-disallowed / safe-url/userinfo-disallowed / safe-url/idn-homograph / safe-url/bad-opt. Operator code that wants a boolean parse-without-throw shape wraps the throw in a try / catch.
b.safeUrl.ALLOW_HTTP_TLS #
Frozen protocol allowlist for HTTPS-only HTTP traffic — ["https:"]. The framework default for any outbound URL parsed without an explicit opts.allowedProtocols. Operators with a legitimate cleartext use case opt in per call via ALLOW_HTTP_ALL.
var b = require("blamejs");
b.safeUrl.ALLOW_HTTP_TLS;
// → ["https:"]
b.safeUrl.ALLOW_HTTP_ALL #
Frozen protocol allowlist accepting both HTTP and HTTPS — ["http:", "https:"]. Pass to parse when the call site legitimately speaks cleartext (loopback admin endpoints, on-prem service mesh terminating TLS at a sidecar, legacy partner APIs). Never the framework default — TLS-required is.
var b = require("blamejs");
var u = b.safeUrl.parse("http://127.0.0.1:8080/health", {
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL,
});
u.protocol;
// → "http:"
b.safeUrl.ALLOW_WS_TLS #
Frozen protocol allowlist for secure WebSocket traffic — ["wss:"]. The framework default for any WebSocket URL parsed without an explicit opts.allowedProtocols.
var b = require("blamejs");
b.safeUrl.ALLOW_WS_TLS;
// → ["wss:"]
b.safeUrl.ALLOW_WS_ALL #
Frozen protocol allowlist accepting both ws: and wss: — ["ws:", "wss:"]. Opt-in per call when cleartext WebSocket is acceptable (loopback dev, sidecar-terminated TLS).
var b = require("blamejs");
var u = b.safeUrl.parse("ws://127.0.0.1:9000/stream", {
allowedProtocols: b.safeUrl.ALLOW_WS_ALL,
});
u.protocol;
// → "ws:"
b.safeUrl.ALLOW_ANY #
Frozen allowlist accepting every framework-supported scheme — ["http:", "https:", "ws:", "wss:"]. Suited to a generic URL-validation surface where the caller already enforces the protocol downstream; narrower allowlists are preferred wherever possible.
var b = require("blamejs");
b.safeUrl.ALLOW_ANY.length;
// → 4
b.safeUrl.SafeUrlError #
Error class thrown by parse (or by the caller-supplied opts.errorClass, used by b.objectStore / b.logStream / b.httpClient to surface a decorated operational error type). Extends FrameworkError. Carries a stable .code: safe-url/missing / safe-url/too-long / safe-url/malformed / safe-url/protocol-disallowed / safe-url/userinfo-disallowed / safe-url/idn-homograph / safe-url/uncanonicalizable / safe-url/bad-opt. HTTP middleware inspects .code to translate the throw into a 400 without leaking parser internals.
var b = require("blamejs");
try {
b.safeUrl.parse("ftp://example.com/file.txt");
} catch (e) {
e instanceof b.safeUrl.SafeUrlError; // → true
e.code; // → "safe-url/protocol-disallowed"
}
b.safeUrl.parse(url, opts?) #
{
allowedProtocols: string[], // default ALLOW_HTTP_TLS (["https:"])
maxUrlLength: number, // default 8192 (RFC 7230 §3.1.1)
allowUserinfo: boolean, // default false; opt-in to user:pass@
allowMixedScript: boolean, // default false; opt-in to mixed-script labels
allowedScripts: string[], // narrow mixed-script allowlist (e.g. ["latin","cyrillic"])
errorClass: Function, // throw this instead of SafeUrlError (used by b.httpClient / b.objectStore)
}
Parse a URL string (or an existing URL instance) through the framework's defensive gates: length cap BEFORE Node's WHATWG parser sees the input (RFC 7230 §3.1.1 — 8 KiB default), protocol allowlist (https: only by default), user:pass@ userinfo refusal (credentials leak into request logs / error messages / metric labels / trace spans), and per-label IDN-homograph defense (UTS #39 §5 mixed-script — Cyrillic 'о' inside an otherwise-Latin label). Returns the parsed URL instance on success.
Throws SafeUrlError (or the caller-supplied opts.errorClass) with one of the documented .code strings: safe-url/missing / safe-url/too-long / safe-url/malformed / safe-url/protocol-disallowed / safe-url/userinfo-disallowed / safe-url/idn-homograph / safe-url/bad-opt. Operator code that wants a boolean parse-without-throw shape wraps the call in a try / catch.
var b = require("blamejs");
// Default: HTTPS-only, length cap, userinfo refused, IDN-homograph defended.
var u = b.safeUrl.parse("https://example.com/path?q=1");
u.hostname;
// → "example.com"
// Cleartext is opt-in per call via the ALLOW_HTTP_ALL preset.
var http = b.safeUrl.parse("http://127.0.0.1:8080/health", {
allowedProtocols: b.safeUrl.ALLOW_HTTP_ALL,
});
http.protocol;
// → "http:"
// Disallowed protocol throws SafeUrlError.
try { b.safeUrl.parse("javascript:alert(1)"); }
catch (e) { e.code; }
// → "safe-url/protocol-disallowed"
// Userinfo refused by default — credentials belong in headers.
try { b.safeUrl.parse("https://alice:s3cr3t@example.com/"); }
catch (e) { e.code; }
// → "safe-url/userinfo-disallowed"
// Boolean parse-without-throw shape via try/catch wrapper.
function isValid(s) {
try { b.safeUrl.parse(s); return true; }
catch (_e) { return false; }
}
isValid("https://example.com/"); // → true
isValid("ftp://example.com/"); // → false
b.safeUrl.format(url) #
Defensive wrapper around URL formatting that translates the assertion-class throw documented in [CVE-2026-21712](https://nvd.nist.gov/vuln/detail/CVE-2026-21712) (IDN crash via legacy url.format()) into a typed safe-url/format-failed refusal. Accepts either a string URL or a URL instance; returns the canonical string form.
var out = b.safeUrl.format("https://example.com/a?q=1");
// → "https://example.com/a?q=1"
b.safeUrl.canonicalize(input, opts?) #
{
allowedSchemes: string[], // default ALLOW_ANY (http/https/ws/wss); canonicalize is a compare tool, not a fetch gate
allowUserinfo: boolean, // default false; opt-in to keep user:pass@ (still discouraged)
allowMixedScript: boolean, // default false; opt-in to mixed-script host labels
allowedScripts: string[], // narrow mixed-script allowlist (e.g. ["latin","cyrillic"])
maxUrlLength: number, // default 8192 (RFC 7230 §3.1.1)
errorClass: Function, // throw this instead of SafeUrlError
}
Return the single canonical, comparable form of a URL so two spellings of the same destination compare equal as strings. The use cases are host allowlists, dedup / cache keys, and SSRF pre-checks — exactly the places an attacker reaches for an obfuscated host (http://0177.0.0.1/, http://2130706433/, http://[::ffff:7f00:1]/, an IDN homograph, a trailing-dot or default-port variation) to slip past a naive === allowlist. Routing every comparison through one audited canonicalizer closes that class instead of leaving each caller to re-derive normalization (which is how the bypasses happen).
The canonical form is built from parse's defensive gates plus the security-relevant normalization set:
- Scheme and host lowercased (the WHATWG URL parser does this). - Host IDN labels emitted as their punycode xn-- A-label; a mixed-script / confusable host label THROWS exactly as parse does (a homograph is never silently passed) unless the caller opts in via allowMixedScript / allowedScripts. - A trailing dot on the host is removed (example.com. → example.com) — DNS-equivalent but breaks string comparison. - An IP-literal host in ANY notation collapses to one canonical string via b.ssrfGuard.canonicalizeHost (the SAME byte parser the SSRF classifier matches on): IPv4 decimal / octal / hex / shorthand → dotted-quad; IPv6 (incl. IPv4-mapped + any zero-compression) → RFC 5952 lower-hex, bracketed. - The default port for the scheme is stripped (:80 http/ws, :443 https/wss — the parser does this). - Path . / .. segments resolved (WHATWG), then RFC 3986 §6.2.2 percent-normalization applied to the path: hex digits uppercased, escapes of unreserved characters decoded. Query and fragment are left BYTE-FOR-BYTE as parsed — reordering or re-decoding there can change application semantics.
Throws SafeUrlError (or opts.errorClass): the parse codes for a missing / too-long / malformed / disallowed-scheme / userinfo / homograph input, plus safe-url/uncanonicalizable when a parsed URL cannot be reduced to a safe canonical form. This is a config / entry-point validator — it THROWS on bad input, it does NOT return a best-effort string.
var b = require("blamejs");
// Every obfuscated loopback spelling collapses to one string.
b.safeUrl.canonicalize("http://0177.0.0.1/"); // → "http://127.0.0.1/"
b.safeUrl.canonicalize("http://2130706433/"); // → "http://127.0.0.1/"
b.safeUrl.canonicalize("http://127.1/"); // → "http://127.0.0.1/"
// Case, default port, trailing dot, and `..` all normalize.
b.safeUrl.canonicalize("https://Example.COM:443/a/../b");
// → "https://example.com/b"
// A disallowed scheme throws SafeUrlError.
try { b.safeUrl.canonicalize("ftp://example.com/"); }
catch (e) { e.code; } // → "safe-url/protocol-disallowed"
Last updated 2026-08-08T16:39:49.652Z by seeder.