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
This commit is contained in:
wt committed 2026-10-15 02:31:33 +07:00
1 parent f556fc0deb
commit e2b4dbdb76
19 files changed
+1648 -8

No files matched your search

@@ -0,0 +1,96 @@
# Bounded standalone XML ingress experiment (S00-018)
This fixture experiment uses real Stanza 12.22.1 Client dispatch, JXT.parse and
registry.import, preceded by saxes 6.0.0 XML 1.0 namespace-aware validation.
It adds a constructor to the public Client.transports registry and selects only
that transport via configuration. It does not use the stock persistent parser,
subclass private transport fields, intercept already imported events, or patch
vendor files. It is not a production transport or a library selection.
From project root, reuse the reviewed isolated Node runtime (macOS ARM64):
```sh
export PATH=/private/tmp/luma-stack-runtime/node-v24.21.0-darwin-arm64/bin:$PATH
npm ci --prefix experiments/0002-xmpp-library/parser-validation --ignore-scripts --no-audit --no-fund --cache /private/tmp/luma-parser-review-cache --fetch-retries=0 --fetch-timeout=15000
npm test --prefix experiments/0002-xmpp-library/parser-validation
python3 experiments/0002-xmpp-library/parser-validation/check-dependencies.py
node experiments/0002-xmpp-library/parser-probe/reproduce.mjs
```
The last command reproduces stock defects; exit 0 is NOT safe parsing.
The dependency checker also requires the original candidate already installed in
experiments/0002-xmpp-library/node_modules for byte comparison. It checks all 340
Stanza files and ten exact locked installed package licenses/integrities. The
saxes tarball omits LICENSE: the upstream v6.0.0 license is preserved locally,
with source https://raw.githubusercontent.com/lddubeau/saxes/v6.0.0/LICENSE.
## Enforcement order and local policy
1. Require primitive string. Reject Blob/ArrayBuffer/typed arrays before coercion.
2. Reject more than 4096 UTF-16 units without encoding; then reject more than
4096 UTF-8 bytes using TextEncoder. The temporary encoding buffer is bounded
by the unit check (at most 12288 bytes for any input); it is not an unbounded
copy. First character must be `<` as required by RFC 7395.
3. New saxes parser per message, fragment=false, xmlns=true, XML 1.0 forced,
no inherited namespace resolver. Throw on the first error; close() checks
document completion. No tree is built during this phase.
4. At opentagstart reject depth >8 (including root), elements >32, name >128
UTF-16 units. At attribute reject total attributes >16 (including xmlns),
name >128, decoded value >256, or aggregate decoded attribute name+value
units >1024. Checks occur before JXT tree construction/import. Saxes has
already stored one completed attribute at its callback; lexical tokens,
namespace tables and DTD buffers before callbacks are bounded by 4096 input
bytes. No claim of allocation-free SAX processing is made.
5. Reject DTDs, comments, processing instructions and XML declarations. Only
built-in/numeric entities are available; no DTD fetching/entity expansion is
enabled. CDATA is accepted. Declaration rejection is local conservative
policy; RFC 7395 discourages but does not forbid declarations.
6. Only after complete validation call real JXT.parse and registry.import. An
unknown top-level root or candidate parser/import error closes the attempt.
Parsed output is delivered through Client stream:data only after import
succeeds. No invalid document's earlier root is emitted.
7. On rejection close the socket, detach all four callbacks, clear config and
stream references before emitting a payload-free error code. Every old
captured callback checks the terminal flag. Parsers/trees are synchronous
function locals, never retained between messages. A fresh Client.connect
creates another transport; restart on a failed transport cannot revive it.
The combined boundary fixture hits 4096 bytes, depth 8, 32 elements,
16 attributes and 1024 attribute units simultaneously and imports a real body.
Separate tests cover name/value equality, multibyte byte equality, one-over
bounds, stock 1 MiB/depth-256/18-message cases, malformed tails, incomplete XML,
binary, duplicate attributes/namespaces/entities/invalid characters, callback
replay, fresh connect, stream open/close/restart and outgoing registry export.
Tracing registry.import calls the real implementation; it never fakes success.
FixtureSocket replaces only browser socket effects; tests use actual Client
connect/transport selection and stanza queue events.
## Constraints
The default socket constructor is globalThis.WebSocket; browser use requires a
CommonJS-capable bundler for Stanza/saxes/xmlchars, native WebSocket, URL and
TextEncoder plus support for private class fields. The injected fixture socket
is explicitly separate and never chosen by the default path. Only explicit
wss URLs and the xmpp subprotocol are allowed. TLS/CORS/binding are unchanged;
no real WSS handshake is exercised here. Browser build/runtime and selected
browser versions are NOT verified. Node tests run 24.21.0/npm 11.19.0.
Limits protect application parsing after the browser delivers a complete message.
They cannot prevent the browser/network stack from allocating a huge inbound
message, nor bound total traffic, Client queue growth, CPU rate, reconnect
policy or downstream extension work. Those need separate product/provider
policies. The experiment closes locally on violations rather than implementing
full XMPP stream-error reporting/close negotiation/redirect/keepalive behavior.
It does not enforce complete XMPP stream sequencing or preserve inherited stream
language. JXT interpretation/normalization is retained; SAX and JXT equivalence
for all accepted XML forms is not established. No auth/crypto changes, complete
NET-003 policy, OMEMO, independent server/client interoperability, exhaustion
benchmark, advisory/provenance audit, or release acceptance is claimed.
All ten package license texts/notices are in THIRD_PARTY_NOTICES.md and exact
versions in LICENSE_INVENTORY.md/package-lock.json. Saxes is a mature existing
strict parser with documented XML/namespace conformance and public event APIs;
6.0.0 is an older release, and no ongoing-maintenance SLA is assumed. Its
absence of built-in token limits is why the mandatory byte precheck comes first.
See proposed docs/decisions/0002-xmpp-parser-validation.md. Independent security
review is required; S00-003 remains a separate assignment after reviewed done.