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:
1 parent
f556fc0deb
commit
e2b4dbdb76
19 files changed
+1648
-8
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {test} from 'node:test';
|
||||
import clientModule from 'stanza/Client.js';
|
||||
import {installBoundedTransport, validateMessage, LIMITS} from '../bounded-ingress.mjs';
|
||||
import {opening, closing, message, valid, deep, elements, attributes, boundary, invalid} from './fixtures.mjs';
|
||||
const Client = clientModule.default;
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
async function harness() {
|
||||
const sockets = [];
|
||||
class FixtureSocket {
|
||||
readyState = 1; protocol = 'xmpp'; sent = []; closes = 0;
|
||||
constructor(url, protocol) { this.url = url; this.requestedProtocol = protocol; sockets.push(this); }
|
||||
send(data) { this.sent.push(data); }
|
||||
close() { this.closes++; this.readyState = 3; }
|
||||
}
|
||||
const client = new Client({autoReconnect: false, server: 'example.invalid'});
|
||||
installBoundedTransport(client, FixtureSocket);
|
||||
client.updateConfig({transports: {'bounded-websocket': 'wss://example.invalid/xmpp'},
|
||||
transportPreferenceOrder: ['bounded-websocket']});
|
||||
const received = [], errors = [], raw = [], stanzas = [], starts = [];
|
||||
client.on('stream:data', (stanza, kind) => received.push({stanza, kind}));
|
||||
client.on('stanza', stanza => stanzas.push(stanza));
|
||||
client.on('stream:start', stanza => starts.push(stanza));
|
||||
client.on('ingress:rejected', code => errors.push(code));
|
||||
client.on('raw', (...args) => raw.push(args));
|
||||
// Tracing calls the real registry.import; proves rejection before import.
|
||||
let imports = 0;
|
||||
const original = client.stanzas.import.bind(client.stanzas);
|
||||
client.stanzas.import = (...args) => { imports++; return original(...args); };
|
||||
await client.connect();
|
||||
const socket = sockets.at(-1);
|
||||
socket.onopen();
|
||||
return {client, sockets, socket, received, errors, raw, stanzas, starts,
|
||||
imports: () => imports, finish: async () => {client.transport?.disconnect(false); await tick();}};
|
||||
}
|
||||
|
||||
for (const [name, xml, code] of invalid) {
|
||||
test(`terminal rejection: ${name}`, async () => {
|
||||
const h = await harness();
|
||||
const late = h.socket.onmessage;
|
||||
late({data: xml});
|
||||
assert.deepEqual(h.errors, [code]);
|
||||
assert.equal(h.socket.closes, 1);
|
||||
assert.equal(h.socket.onmessage, null);
|
||||
assert.equal(h.received.length, 0);
|
||||
assert.equal(h.starts.length, 0);
|
||||
if (code !== 'unknown-root') assert.equal(h.imports(), 0);
|
||||
// Old captured handlers and subsequent messages cannot revive an attempt.
|
||||
late({data: closing}); late({data: valid});
|
||||
await tick();
|
||||
assert.equal(h.stanzas.length, 0);
|
||||
assert.equal(h.received.length, 0);
|
||||
assert.deepEqual(h.errors, [code]);
|
||||
assert.equal(h.client.transport, undefined);
|
||||
assert.equal(h.raw.length, 0);
|
||||
await h.finish();
|
||||
});
|
||||
}
|
||||
|
||||
for (const [name, xml, body] of [
|
||||
['all aggregate limits equal', boundary().xml, boundary().body],
|
||||
['depth equal', deep(LIMITS.depth)],
|
||||
['element count equal', elements(LIMITS.elements)],
|
||||
['attribute count equal', attributes(LIMITS.attributes)],
|
||||
['attribute value equal', `<message xmlns="jabber:client" a="${'v'.repeat(256)}"/>`],
|
||||
['name equal', `<message xmlns="jabber:client"><${'x'.repeat(128)}/></message>`],
|
||||
['UTF-8 byte equality', message('🦉'.repeat(1000) + 'a'.repeat(4096 - message('').length - 4000)), '🦉'.repeat(1000) + 'a'.repeat(4096 - message('').length - 4000)],
|
||||
['escaped text and CDATA', message('<&<![CDATA[<fixture>]]>'), '<&<fixture>'],
|
||||
['prefixed namespace', '<c:message xmlns:c="jabber:client"><c:body>namespace</c:body></c:message>', 'namespace']
|
||||
]) {
|
||||
test(`actual parser/import accepts ${name}`, async () => {
|
||||
const h = await harness();
|
||||
h.socket.onmessage({data: xml});
|
||||
await tick();
|
||||
assert.deepEqual(h.errors, []);
|
||||
assert.equal(h.received.length, 1);
|
||||
assert.equal(h.received[0].kind, 'message');
|
||||
if (body !== undefined) assert.equal(h.received[0].stanza.body, body);
|
||||
assert.equal(h.stanzas.length, 1);
|
||||
assert.ok(h.imports() > 0);
|
||||
await h.finish();
|
||||
});
|
||||
}
|
||||
|
||||
test('bytes one over and UTF-8 one over reject before import', async () => {
|
||||
for (const xml of [boundary().xml + ' ', message('🦉'.repeat(1000) + 'a'.repeat(4097 - message('').length - 4000))]) {
|
||||
const h = await harness(); h.socket.onmessage({data: xml});
|
||||
assert.deepEqual(h.errors, ['bytes']); assert.equal(h.imports(), 0);
|
||||
await h.finish();
|
||||
}
|
||||
});
|
||||
|
||||
test('stock 18-message partial document never accumulates or revives; fresh connect works', async () => {
|
||||
const h = await harness(); const old = h.socket.onmessage;
|
||||
old({data: opening});
|
||||
for (let i = 0; i < 16; i++) old({data: 'a'.repeat(4096)});
|
||||
old({data: closing});
|
||||
await tick();
|
||||
assert.equal(h.received.length, 0); assert.equal(h.imports(), 0);
|
||||
assert.deepEqual(h.errors, ['invalid-xml']);
|
||||
await h.client.connect(); const fresh = h.sockets.at(-1); fresh.onopen();
|
||||
old({data: valid}); assert.equal(h.received.length, 0);
|
||||
fresh.onmessage({data: valid}); await tick();
|
||||
assert.equal(h.received.length, 1); assert.equal(h.stanzas.length, 1);
|
||||
assert.equal(h.received[0].stanza.body, 'finite fixture');
|
||||
await h.finish();
|
||||
});
|
||||
|
||||
test('successive complete documents have no shared namespace/parser state', async () => {
|
||||
const h = await harness();
|
||||
h.socket.onmessage({data: valid}); h.socket.onmessage({data: valid}); await tick();
|
||||
assert.equal(h.stanzas.length, 2);
|
||||
h.socket.onmessage({data: '<c:message/>'}); await tick();
|
||||
assert.deepEqual(h.errors, ['invalid-xml']); assert.equal(h.stanzas.length, 2);
|
||||
await h.finish();
|
||||
});
|
||||
|
||||
test('open, restart, message, outgoing export and close use actual registry', async () => {
|
||||
const h = await harness();
|
||||
h.socket.onmessage({data: '<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" from="example.invalid" version="1.0"/>'});
|
||||
assert.equal(h.client.transport.hasStream, true); assert.equal(h.starts.length, 1);
|
||||
h.client.transport.restart(); assert.equal(h.client.transport.hasStream, false);
|
||||
await h.client.transport.send('message', {body: 'outgoing fixture'});
|
||||
assert.ok(h.socket.sent.at(-1).includes('outgoing fixture'));
|
||||
h.socket.onmessage({data: '<close xmlns="urn:ietf:params:xml:ns:xmpp-framing"/>'});
|
||||
await tick(); assert.equal(h.socket.closes, 1); assert.equal(h.client.transport, undefined);
|
||||
assert.equal(h.errors.length, 0);
|
||||
await h.finish();
|
||||
});
|
||||
|
||||
test('failed subprotocol, socket error, clean disconnect and stale callbacks are terminal', async () => {
|
||||
for (const mode of ['subprotocol', 'error', 'clean']) {
|
||||
const h = await harness(); const old = h.socket.onmessage;
|
||||
if (mode === 'subprotocol') {h.socket.protocol = 'wrong'; h.socket.onopen();}
|
||||
if (mode === 'error') h.socket.onerror(new Error('fixture'));
|
||||
if (mode === 'clean') {
|
||||
h.socket.onmessage({data: '<open xmlns="urn:ietf:params:xml:ns:xmpp-framing"/>'});
|
||||
h.client.transport.disconnect(); assert.ok(h.socket.sent.at(-1).includes('close'));
|
||||
}
|
||||
old({data: valid}); await tick();
|
||||
assert.equal(h.received.length, 0); assert.equal(h.socket.closes, 1);
|
||||
await h.finish();
|
||||
}
|
||||
});
|
||||
|
||||
test('SAX framing validation retains no attempt object', () => {
|
||||
assert.throws(() => validateMessage(opening));
|
||||
assert.throws(() => validateMessage(closing));
|
||||
assert.doesNotThrow(() => validateMessage(valid));
|
||||
});
|
||||
Reference in new issue
Block a user