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

117 lines
7.4 KiB
Markdown

# 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](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.
## 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.