snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
298
tests/prosody/mod_presence_identity_spec.js
Normal file
298
tests/prosody/mod_presence_identity_spec.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import { xml } from '@xmpp/client';
|
||||
import assert from 'assert';
|
||||
|
||||
import { mintToken } from './helpers/jwt.js';
|
||||
import { createXmppClient } from './helpers/xmpp_client.js';
|
||||
|
||||
const CONFERENCE = 'conference-identity.localhost';
|
||||
|
||||
let _roomCounter = 0;
|
||||
const room = () => `presence-identity-${++_roomCounter}@${CONFERENCE}`;
|
||||
|
||||
/**
|
||||
* Connects to the hs256.localhost VirtualHost with a signed token.
|
||||
* mod_presence_identity is loaded on this VirtualHost.
|
||||
*
|
||||
* @param {string} roomJid full room JID, e.g. 'room@conference-identity.localhost'
|
||||
* @param {object} [tokenPayload] JWT payload overrides (e.g. { context: { user: ... } }).
|
||||
* @returns {Promise<XmppTestClient>}
|
||||
*/
|
||||
function hs256Client(roomJid, tokenPayload = {}) {
|
||||
const roomName = roomJid.split('@')[0];
|
||||
const token = mintToken({ room: roomName,
|
||||
...tokenPayload });
|
||||
|
||||
return createXmppClient({ domain: 'hs256.localhost',
|
||||
params: { token } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first presence stanza in the observer's queue that comes from
|
||||
* the given room and is NOT the observer's own self-presence.
|
||||
*
|
||||
* @param {XmppTestClient} observer
|
||||
* @param {string} roomJid
|
||||
* @param {string} observerNick
|
||||
*/
|
||||
function waitForOtherPresence(observer, roomJid, observerNick) {
|
||||
return observer.waitForPresence(
|
||||
p => p.attrs.from?.startsWith(`${roomJid}/`)
|
||||
&& p.attrs.from !== `${roomJid}/${observerNick}`
|
||||
);
|
||||
}
|
||||
|
||||
describe('mod_presence_identity', () => {
|
||||
|
||||
const clients = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(clients.map(c => c.disconnect()));
|
||||
clients.length = 0;
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// identity injection
|
||||
// -------------------------------------------------------------------------
|
||||
describe('identity injection', () => {
|
||||
|
||||
it('injects <identity><user> from JWT context.user into MUC presence', async () => {
|
||||
const r = room();
|
||||
|
||||
const identity = await hs256Client(r, {
|
||||
context: { user: { id: 'u1',
|
||||
name: 'Alice' } }
|
||||
});
|
||||
|
||||
clients.push(identity);
|
||||
await identity.joinRoom(r);
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
const presence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
const identityEl = presence.getChild('identity');
|
||||
|
||||
assert.ok(identityEl, 'presence must contain <identity>');
|
||||
|
||||
const userEl = identityEl.getChild('user');
|
||||
|
||||
assert.ok(userEl, '<identity> must contain <user>');
|
||||
assert.strictEqual(userEl.getChild('id')?.text(), 'u1', 'id must match token');
|
||||
assert.strictEqual(userEl.getChild('name')?.text(), 'Alice', 'name must match token');
|
||||
});
|
||||
|
||||
it('adds <group> inside <identity> when context.group is set', async () => {
|
||||
const r = room();
|
||||
|
||||
const identity = await hs256Client(r, {
|
||||
context: {
|
||||
user: { id: 'u2' },
|
||||
group: 'mygroup'
|
||||
}
|
||||
});
|
||||
|
||||
clients.push(identity);
|
||||
await identity.joinRoom(r);
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
const presence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
const identityEl = presence.getChild('identity');
|
||||
|
||||
assert.ok(identityEl, 'presence must contain <identity>');
|
||||
assert.strictEqual(identityEl.getChild('group')?.text(), 'mygroup',
|
||||
'<group> must match token context.group');
|
||||
});
|
||||
|
||||
it('does not inject <identity> when token has no context.user', async () => {
|
||||
const r = room();
|
||||
|
||||
// Valid token but no context field → no jitsi_meet_context_user on session.
|
||||
const noContext = await hs256Client(r);
|
||||
|
||||
clients.push(noContext);
|
||||
await noContext.joinRoom(r);
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
const presence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
|
||||
assert.ok(!presence.getChild('identity'),
|
||||
'presence must not contain <identity> when token has no context.user');
|
||||
});
|
||||
|
||||
it('injects <identity> in presence updates (not only on join)', async () => {
|
||||
const r = room();
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
const identity = await hs256Client(r, {
|
||||
context: { user: { id: 'u3',
|
||||
name: 'Bob' } }
|
||||
});
|
||||
|
||||
clients.push(identity);
|
||||
await identity.joinRoom(r);
|
||||
|
||||
// Consume the join presence.
|
||||
const joinPresence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
|
||||
assert.ok(joinPresence.getChild('identity'), 'join presence must have <identity>');
|
||||
|
||||
// Send a presence update (no <x muc> element — this is a regular update,
|
||||
// not a join). Both pre-presence/bare and pre-presence/full hooks fire.
|
||||
const identityNick = joinPresence.attrs.from.split('/')[1];
|
||||
|
||||
await identity.sendPresence(`${r}/${identityNick}`);
|
||||
|
||||
// The update must also carry <identity>.
|
||||
const updatePresence = await observer.waitForPresence(
|
||||
p => p.attrs.from === `${r}/${identityNick}`
|
||||
);
|
||||
|
||||
assert.ok(updatePresence.getChild('identity'),
|
||||
'presence update must also carry <identity>');
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// malformed JWT context.user keys
|
||||
// -------------------------------------------------------------------------
|
||||
//
|
||||
// update_presence_identity() iterates over JWT context.user keys and uses
|
||||
// each key as an XML element name: presence:tag(key):text(tostring(value)).
|
||||
// Fixed in 2e8980d2b: keys that do not match the XML name pattern
|
||||
// ^[%a_][%w%-%.%:_]*$ are now silently skipped before reaching tag().
|
||||
// This test verifies that bad keys are dropped and valid keys are preserved.
|
||||
|
||||
describe('malformed JWT context.user keys', () => {
|
||||
|
||||
it('survives context.user keys that contain XML-special characters', async () => {
|
||||
const r = room();
|
||||
|
||||
// Key names with '<' and '>' — these become XML element names.
|
||||
// Prosody / LuaXML may escape, drop, or crash on them.
|
||||
const identity = await hs256Client(r, {
|
||||
context: {
|
||||
user: {
|
||||
id: 'safe-id',
|
||||
'bad<key>': 'value'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clients.push(identity);
|
||||
await identity.joinRoom(r);
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
// If Prosody crashed the hook the join presence would never arrive.
|
||||
const presence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
|
||||
assert.ok(presence, 'join presence must arrive even with unusual key names');
|
||||
|
||||
// The safe 'id' element must be present and correct.
|
||||
const identityEl = presence.getChild('identity');
|
||||
|
||||
assert.ok(identityEl, 'presence must still contain <identity>');
|
||||
assert.strictEqual(
|
||||
identityEl.getChild('user')?.getChild('id')
|
||||
?.text(),
|
||||
'safe-id',
|
||||
'safe id field must be present and unmodified'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// spoofing prevention
|
||||
// -------------------------------------------------------------------------
|
||||
describe('spoofing prevention', () => {
|
||||
|
||||
it('strips a client-supplied <identity> when session has no context.user', async () => {
|
||||
const r = room();
|
||||
|
||||
// Token with no context → no jitsi_meet_context_user on session.
|
||||
// The client manually crafts an <identity> element in the join presence.
|
||||
const intruder = await hs256Client(r);
|
||||
|
||||
clients.push(intruder);
|
||||
|
||||
await intruder.joinRoom(r, undefined, {
|
||||
extensions: [
|
||||
xml('identity', {},
|
||||
xml('user', {},
|
||||
xml('id', {}, 'hacker'),
|
||||
xml('name', {}, 'Evil Corp')
|
||||
)
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
const presence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
|
||||
assert.ok(!presence.getChild('identity'),
|
||||
'module must strip the hand-crafted <identity> when no context.user is set');
|
||||
});
|
||||
|
||||
it('strips a client-supplied <identity> and replaces it with the real one', async () => {
|
||||
const r = room();
|
||||
|
||||
// Token with real context.user but also a crafted <identity> in the stanza.
|
||||
// The module must strip the crafted one and inject the real one from the session.
|
||||
const identity = await hs256Client(r, {
|
||||
context: { user: { id: 'real-id',
|
||||
name: 'Real User' } }
|
||||
});
|
||||
|
||||
clients.push(identity);
|
||||
|
||||
await identity.joinRoom(r, undefined, {
|
||||
extensions: [
|
||||
xml('identity', {},
|
||||
xml('user', {},
|
||||
xml('id', {}, 'spoofed-id'),
|
||||
xml('name', {}, 'Spoofed Name')
|
||||
)
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
const observer = await createXmppClient();
|
||||
|
||||
clients.push(observer);
|
||||
await observer.joinRoom(r);
|
||||
|
||||
const presence = await waitForOtherPresence(observer, r, observer.nick);
|
||||
const identityEl = presence.getChild('identity');
|
||||
|
||||
assert.ok(identityEl, 'presence must contain <identity>');
|
||||
assert.strictEqual(identityEl.getChild('user')?.getChild('id')
|
||||
?.text(), 'real-id',
|
||||
'id must be from the verified JWT context, not the spoofed stanza');
|
||||
assert.strictEqual(identityEl.getChild('user')?.getChild('name')
|
||||
?.text(), 'Real User',
|
||||
'name must be from the verified JWT context, not the spoofed stanza');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user