Files
LumaWeb/docs/decisions/0002-xmpp-parser-validation.md
T
wt e2b4dbdb76 S00-018: Bound standalone XML ingress before Stanza parsing and import
Changes: Add an isolated public transport integration with finite byte, depth, element and attribute budgets; validate complete text XML with pinned saxes before real JXT parsing/import. Add adversarial and boundary fixtures, terminal cleanup tests, dependency notices and a proposed parser ADR.
Validation: Fresh-cache npm ci and npm test passed all 55 tests with Node 24.21.0/npm 11.19.0. Reproduced three stock parser failures, checked ten exact dependency licenses/integrities and 340 unchanged Stanza files; make check passed 72 tooling tests. Scope and whitespace inspected. Browser runtime, live WSS and interoperability were not tested.

Task: S00-018
Agent: auto-protocol-89fe6563092c
Role: protocol
Action: claim
Status: review

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

101 lines
6.4 KiB
Markdown

# ADR 0002 parser remediation: bounded standalone XML ingress
- Status: proposed; independent security review pending.
- Task: S00-018 (remediation of S00-003).
- Date: 2026-10-15.
- Owner: auto-protocol-89fe6563092c (protocol).
- Reviewer: not yet assigned.
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](https://www.rfc-editor.org/rfc/rfc7395.html#section-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](https://github.com/legastero/stanza/blob/master/src/Client.ts)
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](https://raw.githubusercontent.com/lddubeau/saxes/v6.0.0/LICENSE)
includes ISC lineage and an historical MIT notice. Public API/browser constraints
were checked against [versioned saxes README](https://raw.githubusercontent.com/lddubeau/saxes/v6.0.0/README.md)
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.