Guard Xml

XML content-safety guard — defends against the XXE / billion- laughs / external-entity / XSLT-exec catalog that has remained active for 20+ years and continues to ship CVEs through 2025- 2026. XML attack surface centers on the DOCTYPE subset, where entity declarations and external references convert a benign- looking XML document into a file-disclosure / SSRF / RCE / DoS primitive depending on the parser.

XXE / external entity (XML External Entity) defense: and SYSTEM / PUBLIC identifiers pointing at file:// / http:// / https:// / ftp:// / gopher:// / jar:// / netdoc:// are refused regardless of profile. CVE-2026-24400 AssertJ toXmlDocument default parser, CVE-2025-3225 sitemap parser, CVE-2024-1455 LangChain XXE, and CVE-2024-25062 libxml2 UAF with DTD + XInclude all fit this shape.

Billion-laughs / entity-expansion DoS: + recursive declarations expand exponentially when the parser dereferences. Refused via the blanket rule; parameter entities ( prefix) get an additional out-of-band exfil tag. CVE-2024-8176 libexpat stack overflow on recursive entity expansion + CVE-2025-24928 libxml2 stack overflow on DTD validation track the family.

DTD external-entity refusal: every declaration is refused unconditionally — there is no safe DTD subset that defenders can enumerate against the parser-quirk landscape, so the only stable posture is to reject the surface entirely.

XSLT / processing-instruction exec defense: and other shapes can route the document through an XSLT processor with document() / xsl:include / xsl:import — full file-disclosure + SSRF surface. Flagged under balanced; refused under strict (after the standard declaration is stripped).

XInclude () and xsi:schemaLocation / xsi:noNamespaceSchemaLocation are operator-controlled fetch surfaces; XML signature elements (xmldsig) require operator defense against signature-wrapping attacks. CDATA sections often hide payloads from naive scanners.

Anti-DoS caps: total document size (maxBytes), nesting depth (maxDepth), element count (maxElements), attribute count per element (maxAttrsPerElement), and attribute value length (maxAttrValueBytes).

Bidi / null / control / zero-width character threats route through the shared lib/codepoint-class detector.

Profiles: strict / balanced / permissive. Compliance postures: hipaa / pci-dss / gdpr / soc2. Even under permissive, DOCTYPE / ENTITY / external-entity refusal stays on — the billion-laughs and XXE classes have no safe permissive posture.

b.guardXml.validate(input, opts?) #

stable0.7.15hipaapci-dssgdprsoc2
{
  profile:               "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  doctypePolicy:         "reject"|"audit"|"allow",
  entityPolicy:          "reject"|"audit"|"allow",
  externalEntityPolicy:  "reject"|"audit"|"allow",
  xincludePolicy:        "reject"|"audit"|"allow",
  schemaLocationPolicy:  "reject"|"audit"|"allow",
  processingInstrPolicy: "reject"|"audit"|"allow",
  cdataPolicy:           "reject"|"audit"|"allow",
  xmlDsigPolicy:         "audit"|"allow",
  bidiPolicy:            "reject"|"strip"|"audit"|"allow",
  controlPolicy:         "reject"|"strip"|"allow",
  nullBytePolicy:        "reject"|"strip"|"allow",
  zeroWidthPolicy:       "reject"|"strip"|"audit"|"allow",
  maxBytes:              number,    // total source byte cap
  maxDepth:              number,    // estimated nesting depth cap
  maxElements:           number,    // total open-tag count cap
  maxAttrsPerElement:    number,    // attribute count cap per element
  maxAttrValueBytes:     number,    // per-attr-value length cap
  maxNumericCharRefs:    number,    // numeric character reference cap
}

Inspect input (string of XML source) for the full guard-xml threat catalog without invoking a parser. Returns { ok, issues } where issues enumerates every DOCTYPE declaration, definition (including parameter entities), SYSTEM/PUBLIC external-entity reference, XInclude directive, xsi:schemaLocation hint, processing instruction (after the standard declaration), CDATA section, XML signature element, and codepoint-class threat. Element / depth caps are estimated via tag-count + nesting heuristics — strict-mode rejects exceeding the configured caps without requiring a full parse.

Profile-driven (strict / balanced / permissive) and posture- driven (hipaa / pci-dss / gdpr / soc2). Note that DOCTYPE / / external-entity refusal stays on under every profile — there is no safe permissive posture for the XXE + billion-laughs class.

var hostile = '\n' +
              ']>\n';
var rv = b.guardXml.validate(hostile, { profile: "strict" });
rv.ok;                                              // → false
rv.issues.some(function (i) { return i.kind === "doctype"; });  // → true

b.guardXml.sanitize(input, opts?) #

stable0.7.15
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  bidiPolicy:      "reject"|"strip"|"audit"|"allow",
  controlPolicy:   "reject"|"strip"|"allow",
  nullBytePolicy:  "reject"|"strip"|"allow",
  zeroWidthPolicy: "reject"|"strip"|"audit"|"allow",
}

Best-effort cleanup of input (string of XML source): strips codepoint-class threats per policy (BOM, bidi when bidiPolicy: "strip", C0 controls when controlPolicy: "strip", null bytes when nullBytePolicy: "strip", zero-width characters when zeroWidthPolicy: "strip"). Throws GuardXmlError on any critical issue — DOCTYPE / / external-entity / param- entity shapes have no safe sanitization (the only correct response is refusal). The error code matches the triggering rule (xml.doctype, xml.entity, xml.external-entity, etc.).

Sanitize is intentionally narrow: it cleans the character-class surface but never rewrites structural XML. Use b.guardXml.gate for the full sanitize-or-refuse action chain inside a request pipeline.

// Build hostile input programmatically so the source stays ASCII.
var ZWSP = String.fromCharCode(0x200B);
var clean = b.guardXml.sanitize("hello" + ZWSP + "", {
  profile: "balanced",
});
clean.indexOf(ZWSP) === -1;                         // → true

b.guardXml.gate(opts?) #

stable0.7.15hipaapci-dssgdprsoc2
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  name:       string,    // gate identity for audit / observability
}

Build a b.gateContract gate suitable for plugging into b.staticServe({ contentSafety: { ".xml": gate } }), b.fileUpload({ contentSafety: { "application/xml": gate } }), or any host primitive that consumes the gate-contract shape. Action chain on validation: serve (no issues) → audit-only (warn-only issues) → sanitize (high/critical when DOCTYPE / ENTITY / external-entity policies are not reject, which strips codepoint-class threats only) → refuse (any of those structural policies is reject and a critical issue fired, or sanitize threw).

Under strict and balanced both, DOCTYPE / ENTITY / external-entity are reject — so the gate jumps from audit-only straight to refuse for the XXE / billion-laughs class. Permissive allows downgrading XInclude / schemaLocation / PI / CDATA to audit, but never DOCTYPE / ENTITY / external-entity.

var xmlGate = b.guardXml.gate({ profile: "strict" });
var hostile = Buffer.from(
  '\n]>\n',
  "utf8");
var verdict = await xmlGate.check({ bytes: hostile });
verdict.action;                                     // → "refuse"

b.guardXml.compliancePosture(name) #

stable0.7.15hipaapci-dssgdprsoc2

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 GuardXmlError with code "xml.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.guardXml.compliancePosture("hipaa");
posture;                                             // → overlay clone (mutable)

try {
  b.guardXml.compliancePosture("not-a-regime");
} catch (e) {
  e.code;                                            // → "xml.bad-posture"
}

b.guardXml.buildProfile(opts) #

stable0.7.15
{
  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.guardXml.buildProfile({ extends: "strict" });
custom;                                              // → composed profile object

b.guardXml.loadRulePack(pack) #

stable0.7.15

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 GuardXmlError with code "xml.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.guardXml.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id;                                             // → "tenant-policy"

b.guardXml.resolveOpts(opts?) #

stable0.7.15
{
  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 GuardXmlError with code "xml.bad-opt" / "xml.bad-posture" on an unknown profile or posture name.

var resolved = b.guardXml.resolveOpts({ profile: "strict" });
resolved.profile;                                    // → "strict"

Last updated 2026-08-08T16:39:49.652Z by seeder.