snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
165
tests/prosody/lua/rayo_dial_header_crlf_spec.lua
Normal file
165
tests/prosody/lua/rayo_dial_header_crlf_spec.lua
Normal file
@@ -0,0 +1,165 @@
|
||||
-- Unit test for Rayo dial IQ header serialization.
|
||||
-- Run with busted from resources/prosody-plugins/:
|
||||
-- busted spec/lua/
|
||||
--
|
||||
-- Purpose: a Rayo "dial" IQ (urn:xmpp:rayo:1) carries client-influenced header
|
||||
-- values (e.g. JvbRoomName / JvbRoomPassword). If a header value contains a raw
|
||||
-- carriage-return / line-feed, an unescaped serialization would let a client
|
||||
-- inject extra lines into the XMPP stream — and, downstream, into the SIP
|
||||
-- headers jigasi builds from these values (CRLF / header injection).
|
||||
--
|
||||
-- Prosody's util.stanza serializer neutralizes this by XML-escaping control
|
||||
-- characters: "\r" -> " " and "\n" -> " ". This spec produces a dial IQ
|
||||
-- with such a header and asserts the serialized stanza contains " " and
|
||||
-- leaks no raw CR/LF bytes.
|
||||
--
|
||||
-- It uses Prosody's real util.stanza when it resolves on LUA_PATH; otherwise it
|
||||
-- falls back to a small in-file serializer that mirrors Prosody's escape table,
|
||||
-- so the contract is still asserted in CI (where Prosody is not installed).
|
||||
|
||||
local RAYO_NS = 'urn:xmpp:rayo:1';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Obtain a stanza builder: prefer the real util.stanza, else a faithful stub.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local st, source;
|
||||
|
||||
local ok_real, real = pcall(require, 'util.stanza');
|
||||
if ok_real and type(real) == 'table' and real.iq then
|
||||
st = real;
|
||||
source = 'util.stanza (real Prosody)';
|
||||
else
|
||||
source = 'in-file reference serializer';
|
||||
|
||||
-- Mirrors Prosody's util.stanza escape table: control characters are
|
||||
-- escaped to numeric entities so they cannot break out of an attribute.
|
||||
local escape_table = {
|
||||
["'"] = "'",
|
||||
['"'] = """,
|
||||
["<"] = "<",
|
||||
[">"] = ">",
|
||||
["&"] = "&",
|
||||
["\t"] = "	",
|
||||
["\n"] = " ",
|
||||
["\r"] = " ",
|
||||
};
|
||||
local function xml_escape(str)
|
||||
return (str:gsub("['&<>\"\t\n\r]", escape_table));
|
||||
end
|
||||
|
||||
local stanza_mt = {};
|
||||
stanza_mt.__index = stanza_mt;
|
||||
|
||||
local function new_stanza(name, attr)
|
||||
return setmetatable({
|
||||
name = name,
|
||||
attr = attr or {},
|
||||
tags = {},
|
||||
last_add = {},
|
||||
}, stanza_mt);
|
||||
end
|
||||
|
||||
function stanza_mt:tag(name, attr)
|
||||
local child = new_stanza(name, attr);
|
||||
local parent = self.last_add[#self.last_add] or self;
|
||||
|
||||
table.insert(parent.tags, child);
|
||||
table.insert(self.last_add, child);
|
||||
|
||||
return self;
|
||||
end
|
||||
|
||||
function stanza_mt:up()
|
||||
table.remove(self.last_add);
|
||||
|
||||
return self;
|
||||
end
|
||||
|
||||
local function serialize(node, buf)
|
||||
table.insert(buf, "<" .. node.name);
|
||||
for k, v in pairs(node.attr) do
|
||||
table.insert(buf, " " .. k .. '="' .. xml_escape(tostring(v)) .. '"');
|
||||
end
|
||||
|
||||
if #node.tags == 0 then
|
||||
table.insert(buf, "/>");
|
||||
else
|
||||
table.insert(buf, ">");
|
||||
for _, child in ipairs(node.tags) do
|
||||
serialize(child, buf);
|
||||
end
|
||||
table.insert(buf, "</" .. node.name .. ">");
|
||||
end
|
||||
|
||||
return buf;
|
||||
end
|
||||
|
||||
stanza_mt.__tostring = function(self)
|
||||
return table.concat(serialize(self, {}));
|
||||
end
|
||||
|
||||
st = {
|
||||
iq = function(attr)
|
||||
return new_stanza('iq', attr);
|
||||
end,
|
||||
};
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Test
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
describe("Rayo dial IQ header CRLF escaping [" .. source .. "]", function()
|
||||
|
||||
-- Builds a dial IQ the same shape mod_muc_jigasi_invite.lua produces:
|
||||
-- <iq><dial xmlns=rayo><header name=... value=.../></dial></iq>
|
||||
-- with a JvbRoomName header whose value is spiked with a raw CRLF.
|
||||
local function build_dial_iq(room_value)
|
||||
return st.iq({
|
||||
xmlns = "jabber:client",
|
||||
type = "set",
|
||||
to = "jvb@example.com",
|
||||
from = "focus@example.com",
|
||||
id = "dial-1",
|
||||
})
|
||||
:tag("dial", { xmlns = RAYO_NS, from = "fromnumber", to = "sip:test@example.com" })
|
||||
:tag("header", { xmlns = RAYO_NS, name = "JvbRoomName", value = room_value })
|
||||
:up()
|
||||
:up();
|
||||
end
|
||||
|
||||
it("escapes a CRLF-injected header value to ", function()
|
||||
-- A client attempting to smuggle an extra SIP header via CRLF injection.
|
||||
local malicious = "room@conference.example.com\r\nInjected-Header: evil";
|
||||
local iq = build_dial_iq(malicious);
|
||||
|
||||
local xml = tostring(iq);
|
||||
|
||||
assert.is_truthy(xml:find(" ", 1, true),
|
||||
"serialized dial IQ must escape the raw CRLF to ");
|
||||
end)
|
||||
|
||||
it("leaks no raw CR or LF bytes in the serialized stanza", function()
|
||||
local malicious = "room@conference.example.com\r\nInjected-Header: evil";
|
||||
local iq = build_dial_iq(malicious);
|
||||
|
||||
local xml = tostring(iq);
|
||||
|
||||
assert.is_nil(xml:find("[\r\n]"),
|
||||
"serialized dial IQ must not contain any raw CR/LF byte");
|
||||
end)
|
||||
|
||||
it("preserves the room name text around the escaped CRLF", function()
|
||||
local malicious = "room@conference.example.com\r\nInjected-Header: evil";
|
||||
local iq = build_dial_iq(malicious);
|
||||
|
||||
local xml = tostring(iq);
|
||||
|
||||
-- The header name and the surrounding value text survive; only the
|
||||
-- control characters are entity-encoded.
|
||||
assert.is_truthy(xml:find("JvbRoomName", 1, true));
|
||||
assert.is_truthy(xml:find("room@conference.example.com Injected%-Header: evil"));
|
||||
end)
|
||||
|
||||
end);
|
||||
Reference in New Issue
Block a user