Files
LumaWeb/docs/decisions/0002-xmpp-parser-validation.md
T
wt 74e1fd87d9 S00-018: Approve bounded XML ingress after independent framing checks
Changes: Accept the isolated S00-018 ingress experiment and bounded ADR; record independent security findings and review handoff. Preserve all browser, live WSS, interoperability and production resource limitations.
Validation: Fresh-cache npm ci and all 55 integrated tests passed on Node 24.21.0/npm 11.19.0; six additional adversarial documents rejected before real JXT.parse with stale callback/fresh attempt checks. Verified ten dependency licenses/integrities, 340 unchanged Stanza files, upstream saxes license bytes and matching digests. Stock defects reproduced; make check passed 72 tooling tests. Initial getter-only tracing attempt failed and is recorded separately. Scope/whitespace inspected; no browser or live WSS test.

Task: S00-018
Agent: auto-security-64dc2d9fa232
Role: security
Action: review
Status: done

Task title: Validate bounded standalone XML ingress for the XMPP candidate
2026-10-15 02:35:56 +07:00

7.4 KiB

ADR 0002 parser remediation: bounded standalone XML ingress

  • Status: accepted for the bounded S00-018 fixture experiment; independent review: state/evidence/S00-018-review.md.
  • Task: S00-018 (remediation of S00-003).
  • Date: 2026-10-15.
  • Owner: auto-protocol-89fe6563092c (protocol).
  • Reviewer: auto-security-64dc2d9fa232 (security), 2026-10-15.

Input: reviewed scope/stack/SASL ADRs, S00-003 parser blocker and bounded probe, shipped Stanza parser/transport declarations and RFC 7395. Output: one isolated runnable ingress experiment in experiments/0002-xmpp-library/parser-validation. It does not select Stanza for the product, define production limits/shared contracts, change authentication, or replace remaining S00-003 evidence.

The stock 12.22.1 transport keeps a StreamParser across WebSocket messages and converts payloads to text. The finite diagnostic reproduced a 1 MiB body, depth 256 and 18 separate writes forming one 64 KiB body without errors. StreamParser has no supported resource-budget options. The standalone-frame obligation is RFC 7395 §3.2/3.3.3. Inference from the shipped transport: stock parser reassembly violates that obligation when each probe write represents a separate WebSocket message. The RFC does not prescribe our numerical limits.

Propose a new public Client.transports constructor registration, selected alone through config. The public Transport interface lists connect/disconnect/restart/ send; Client.connect instantiates the registered constructor with Client, StreamManagement and Registry. This is a supported shipped integration surface, not a private subclass override or ignored vendor patch. Exact shipped API: node_modules/stanza/Client.d.ts, index.d.ts, Client.js, jxt/index.d.ts, jxt/Parser.js and jxt/Registry.js. Upstream Client source is supporting context; installed 12.22.1 declarations/behavior are authoritative for this experiment. Future stability is not promised.

Before any JXT tree/import, require text and finite bytes, then use a new namespace-aware saxes XML 1.0 document parser and close it for completeness. Throw on malformed input or exceeded event counters, reject restricted constructs, and only then run the actual JXT.parse and Registry.import. Local ceilings:

Resource Maximum, inclusive Enforcement
Encoded message 4096 UTF-8 bytes Unit precheck, bounded TextEncoder, before SAX
Depth including root 8 SAX opentagstart
Elements including root 32 SAX opentagstart
Attributes including xmlns 16 total/document SAX attribute
Attribute name+decoded value units 1024 total/document SAX attribute
Element/attribute name units 128 each SAX event
Decoded attribute value units 256 each SAX attribute

SAX itself stores tokens/namespaces before callbacks; their worst-case source is capped by bytes first. No handwritten XML security parser or regex syntax approximation is introduced. XML declarations, comments, PI and DTDs are conservatively rejected; CDATA/built-in/numeric entities work. No entity fetch or expansion extension is enabled. The entire document is validated before any earlier root can emit. Incomplete documents/multiple roots/binary messages are terminal and cannot be completed by subsequent messages. Close detaches callbacks, clears attempt stream/config state and rejects stale callbacks. Fresh connect creates a fresh transport. No payload is logged or raw-evented.

Alternatives rejected: post-import counters (too late), stock persistent parser (no budgets/framing), regex or custom security parser (incorrect/unvetted), DOMParser (tree allocated before structural checks), private source patches (unsupported integration). A strict event parser plus byte ceiling provides a bounded preflight without authoring XML syntax. Double parsing retains actual candidate behavior, with bounded cost; complete normalization equivalence is not established and is not claimed.

New exact dependencies: saxes 6.0.0 (ISC), xmlchars 2.2.0 (MIT); Stanza remains 12.22.1 (MIT). Ten exact installed packages are locked with HTTPS registry URLs, SHA512 integrity, full license inventory and notices. Saxes published tarball omits LICENSE; preserved upstream v6.0.0 license includes ISC lineage and an historical MIT notice. Public API/browser constraints were checked against versioned saxes README and installed saxes.d.ts/source, not assumptions about a parser's strict mode. Sources inspected 2026-10-15. Saxes has an older release; maintenance/advisory clearance is not inferred from version pinning. README conformance claims are upstream claims; our evidence is the bounded tests.

Verification: pinned Node 24.21.0/npm 11.19.0, fresh-cache npm ci, 55 integrated tests passing; aggregate boundary document and Unicode boundary actually import; all original failure categories and adversarial cases reject without stanza emission/late revival. Registry tracing calls the real import. All 340 installed Stanza files byte-match the original candidate; no vendor edits. make check passes 72 tooling tests (foundation only). Commands and setup details are in the experiment README and state/evidence/S00-018-auto-protocol-89fe6563092c.md.

Browser use requires a CommonJS-capable bundler, native WebSocket/URL/TextEncoder and private fields. Default uses browser WebSocket, fixture injection captures socket effects only; actual Client.connect and its queue/registry/parser execute. No browser build/runtime, actual WSS/TLS handshake, independent server/client interoperability or exhaustion benchmark was run. Byte checks begin after browser message allocation, and do not bound aggregate traffic/Client queues or downstream work. This minimal transport does not implement all error reporting, close negotiation, redirect, keepalive, language inheritance or XMPP sequencing. No TLS/CORS bypass, invented channel binding, crypto primitive, production policy or shared contract is added. ACC-004/NET-003/NET-004 remain obligations of the original selection/product work; existing accepted SASL experiment is unchanged.

Independent security reviewer must inspect enforcement order/API integration, repeat clean install/tests/license/vendor checks and record acceptance before this ADR or S00-018 is accepted. Coordinator alone commits/launches further work.

Independent acceptance

Security reviewer auto-security-64dc2d9fa232 independently repeated fresh-cache npm ci and all 55 integrated tests on Node 24.21.0/npm 11.19.0. Ten exact installed dependency licenses/integrities and all 340 unchanged Stanza files were checked; the preserved saxes license byte-matches the versioned upstream HTTPS source. Additional six adversarial inputs rejected before traced real JXT.parse, all four stale socket callbacks left fresh attempts unaffected, and fresh attempts imported the exact aggregate-boundary body. make check passed 72 tooling tests. Public shipped constructor/Transport APIs and enforcement order were inspected. The bounded experiment is accepted; every browser, WSS/interoperability, aggregate resource, sequencing and production-policy limitation above remains. Report records commands and the failed initial tracing attempt. Coordinator owns the review commit and subsequent fresh assignment.