Guard Html
HTML / XSS defense — DOM-clobbering, mXSS, and entity-encoding bypasses. Tag and attribute allowlists; URL-scheme allowlist on every URL-bearing attribute; bidi / control / zero-width stripping. Threat catalog grounded in 2026 sanitizer research (DOMPurify CVE series, OWASP XSS / DOM-Clobbering / HTML5 Security cheat sheets, PortSwigger / Sonar / trace37 mXSS write-ups, html5sec.org).
Three profiles ship — strict / balanced / permissive — plus four compliance postures (hipaa / pci-dss / gdpr / soc2) that compose on top via the strictest-wins rule. b.guardHtml.gate(opts) returns a guard descriptor that plugs into b.fileUpload.contentSafety / b.staticServe.contentSafety / b.guardAll.
Threat catalog covered:
1. Dangerous tags — script / style / link / meta / base / iframe / object / embed / applet / form / input / button / textarea / select / isindex / marquee / blink / layer / ilayer / plaintext / listing / xmp / audio / video / source / track / math / svg / template / noscript / noembed / noframes / portal / dialog / keygen / menuitem / command / frame / frameset. 2. on* event-handler attributes — every attribute matching /^on[a-z]/ is denied unconditionally. 3. Form-override attributes — formaction / formmethod / formenctype / formtarget / formnovalidate (CWE-1021). 4. Iframe inline-HTML — srcdoc on iframe always denied. 5. Custom-element registration — is="..." always denied. 6. CSP-bypass-shaped attributes — nonce / integrity / crossorigin stripped from sanitized output. 7. URL scheme validation on URL-bearing attributes — href / src / action / cite / longdesc / manifest / archive / codebase / data / classid / code / profile / ping / dynsrc / lowsrc / background / poster / icon / xlink:href. Per-profile allowlist; denied schemes (always): javascript / vbscript / livescript / mocha / data (outside image context) / file / mhtml / jar / intent / view-source. 8. CSS-injection inside style="..." values — expression( (IE), behavior: (IE), -moz-binding (Firefox legacy), javascript: / vbscript: / livescript: inside url(), @import, @namespace. 9. DOM clobbering — id and name attributes whose values match a well-known JS global (document / window / location / cookie / __proto__ / constructor / ...) on clobber-prone elements (form / input / button / a / img / iframe / object / embed / select / textarea). 10. mXSS hint detection — namespace-context-shift parents (svg / math), CDATA inside HTML mode, template content fragments with entity-encoded payloads. 11. Unicode bidi (CVE-2021-42574 Trojan Source) inside text and attribute values. 12. C0 control characters, null bytes, zero-width chars — strip-or-reject per profile. 13. IE conditional comments — refused in strict, stripped in balanced. 14.
Threat-detection regex literals are composed PROGRAMMATICALLY from numeric codepoint range tables (BIDI_RANGES / C0_CTRL_RANGES / ZERO_WIDTH_RANGES). The source file never embeds the attack characters themselves.
Sanitize discipline: this module ships a token-level rewriter that preserves the allowlisted tag set and strips the rest. For HOSTILE sources, the documented correct response is validate + reject — not sanitize. mXSS bypasses against any non-DOM sanitizer are a known arms-race; the gate's refuse path is the one with strong invariants. Operators with display-of-untrusted-html requirements should additionally serve content under a strict CSP (default-src 'none' or sandboxed iframe).
b.guardHtml.escapeText(value) #
HTML entity-escape for text-content context. Encodes the five core characters & < > " ' so the result is safe to embed inside an element's text body. null / undefined coerce to an empty string. Use this for plain interpolation of untrusted strings into rendered HTML.
var html = b.guardHtml.escapeText("");
// → "<oops>"
var blank = b.guardHtml.escapeText(null);
// → ""
b.guardHtml.escapeAttr(value) #
HTML entity-escape for attribute-value context. Same five characters as escapeText plus backtick (legacy IE attribute terminator) and = (unquoted-attribute edge). Use this when interpolating an untrusted string between double-quoted attribute delimiters.
var attr = b.guardHtml.escapeAttr('say "hi"');
// → "say "hi""
var ie = b.guardHtml.escapeAttr("a`b=c");
// → "a`b=c"
b.guardHtml.validate(input, opts?) #
{
profile: string, // "strict" | "balanced" | "permissive"
compliancePosture: string, // "hipaa" | "pci-dss" | "gdpr" | "soc2"
allowedTags: Array,
allowedAttrs: Array,
urlSchemes: Array,
allowImageData: boolean,
allowComments: boolean,
bidiPolicy: string, // "reject" | "strip" | "audit"
controlPolicy: string,
nullBytePolicy: string,
zeroWidthPolicy: string,
cssPolicy: string,
domClobberPolicy: string,
mxssHintPolicy: string,
maxBytes: number,
maxAttrValueBytes: number,
maxTagDepth: number,
maxAttrsPerTag: number,
}
Tokenize input (string or Buffer of HTML) and walk every element / attribute against the resolved profile. Returns { ok, issues } where issues is an array of { kind, severity, ruleId, location, snippet } records. Never modifies the input — call sanitize for that. Anti-DoS caps (maxBytes / maxAttrValueBytes / maxTagDepth / maxAttrsPerTag) are validated as positive finite integers; passing Infinity throws.
var rv = b.guardHtml.validate("hi
",
{ profile: "strict" });
rv.ok; // → false
rv.issues[0].kind; // → "dangerous-tag"
rv.issues[0].severity; // → "critical"
var clean = b.guardHtml.validate("just text
",
{ profile: "strict" });
clean.ok; // → true
clean.issues.length; // → 0
b.guardHtml.sanitize(input, opts?) #
{
profile: string, // "strict" | "balanced" | "permissive"
compliancePosture: string, // "hipaa" | "pci-dss" | "gdpr" | "soc2"
allowedTags: Array,
allowedAttrs: Array,
urlSchemes: Array,
allowImageData: boolean,
allowComments: boolean,
maxBytes: number,
maxAttrValueBytes: number,
maxTagDepth: number,
maxAttrsPerTag: number,
}
Token-level rewriter that drops every tag NOT in the resolved profile's allowedTags, every attribute NOT in allowedAttrs, every URL whose scheme falls outside the profile allowlist, every event-handler / form-override / clobbering attribute, and every body of a body-drop tag (script / style / template / svg / math / iframe / object / embed / applet). For HOSTILE sources, prefer validate + refuse — sanitize is best effort against the documented arms-race.
var clean = b.guardHtml.sanitize(
"hi
",
{ profile: "balanced" });
// → "hi
"
// Event-handler attribute is stripped, text content preserved.
var stripped = b.guardHtml.sanitize(
'go',
{ profile: "balanced" });
// → 'go'
b.guardHtml.gate(opts?) #
{
name: string,
profile: string, // "strict" | "balanced" | "permissive"
compliancePosture: string, // "hipaa" | "pci-dss" | "gdpr" | "soc2"
mode: string, // "enforce" | "observe"
allowedTags: Array,
allowedAttrs: Array,
urlSchemes: Array,
bidiPolicy: string,
controlPolicy: string,
cssPolicy: string,
domClobberPolicy: string,
mxssHintPolicy: string,
maxBytes: number,
maxRuntimeMs: number,
}
Returns a guard descriptor that plugs into the framework's content-safety wiring (b.fileUpload.contentSafety / b.staticServe.contentSafety / b.guardAll). The descriptor's check(ctx) resolves to one of four actions: serve (no issues), audit-only (low-severity issues observed), sanitize (sanitized buffer attached when no policy is "reject"), or refuse (critical issue with at least one reject-policy active).
var g = b.guardHtml.gate({ profile: "strict" });
g.name; // → "guardHtml:strict"
// Refuse on tag-budget exceeded — strict profile rejects ", "utf8");
var rv = await g.check({ bytes: hostileBuf, contentType: "text/html" });
rv.ok; // → false
rv.action; // → "refuse"
b.guardHtml.compliancePosture(name) #
Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardHtmlError with code "html.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.
var posture = b.guardHtml.compliancePosture("hipaa");
posture; // → overlay clone (mutable)
try {
b.guardHtml.compliancePosture("not-a-regime");
} catch (e) {
e.code; // → "html.bad-posture"
}
b.guardHtml.buildProfile(opts) #
{
extends: string|string[], // base profile name(s) to compose
...: any guard key, // inline override of resolved keys
}
Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.
var custom = b.guardHtml.buildProfile({ extends: "strict" });
custom; // → composed profile object
b.guardHtml.loadRulePack(pack) #
Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardHtmlError with code "html.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.
var pack = b.guardHtml.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id; // → "tenant-policy"
b.guardHtml.resolveOpts(opts?) #
{
profile: string, // one of PROFILES; default this guard's default
compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
}
Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardHtmlError with code "html.bad-opt" / "html.bad-posture" on an unknown profile or posture name.
var resolved = b.guardHtml.resolveOpts({ profile: "strict" });
resolved.profile; // → "strict"
Last updated 2026-08-08T16:39:49.652Z by seeder.