Files
upstream-jitsi-meet/resources/prosody-plugins/mod_muc_resource_validate.lua
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

128 lines
5.1 KiB
Lua

-- Validates the resourcepart of client JIDs when joining a MUC.
-- Copyright (C) 2024-present 8x8, Inc.
--
-- Hooks into muc-occupant-pre-join and rejects joins whose MUC resource is invalid.
--
-- A valid resource consists only of alphanumerics and underscores and must
-- NOT start with an underscore: ^[a-zA-Z0-9][a-zA-Z0-9_]*$
--
-- When "anonymous_strict" mode is enabled, users on domains whose
-- authentication provider is considered anonymous are additionally required
-- to use a MUC resource equal to the first 8 characters of their real JID
-- username (the UUID prefix assigned by the server).
-- The check is dynamic: the auth provider is read from prosody.hosts at
-- join time, so no static domain list is needed.
--
-- The module also hooks muc-occupant-pre-change and rejects MUC nick (resource)
-- changes: an occupant's resource is its stable identity for the session and
-- must never change. Legitimate display-name changes travel in the presence
-- <nick/> child, not the MUC resource, so they are unaffected.
--
-- Example configuration (place under the MUC component):
--
-- Component "conference.meet.jitsi" "muc"
-- modules_enabled = { "muc_resource_validate" }
-- -- Enable strict anonymous-user check
-- anonymous_strict = true
-- -- Optional: extend the list of auth methods treated as anonymous
-- anonymous_auth_methods = { "anonymous", "jitsi-anonymous", "token", "custom-anon" }
local jid_split = require "util.jid".split;
local st = require "util.stanza";
local util = module:require "util";
local is_healthcheck_room = util.is_healthcheck_room;
-- Valid resource: starts with alphanumeric, followed by zero-or-more alphanumerics / underscores.
local VALID_RESOURCE_PATTERN = "^[a-zA-Z0-9][a-zA-Z0-9_]*$";
local anonymous_strict;
local anonymous_auth_methods;
local function load_config()
anonymous_strict = module:get_option_boolean("anonymous_strict", false);
anonymous_auth_methods = module:get_option_set(
"anonymous_auth_methods", { "anonymous", "jitsi-anonymous", "token" });
end
load_config();
-- Returns true when the domain's authentication provider is considered anonymous.
-- Prosody's usermanager sets host.users.name to the value of the
-- "authentication" config option.
local function is_anonymous_domain(domain)
local host_obj = prosody.hosts[domain];
if not host_obj or not host_obj.users then
return false;
end
return anonymous_auth_methods:contains(host_obj.users.name);
end
-- Returns true (halts event processing) when the join should be rejected.
local function check_resource(event)
local room, origin, stanza = event.room, event.origin, event.stanza;
if is_healthcheck_room(room.jid) then
return;
end
local user, domain = jid_split(stanza.attr.from);
local _, _, muc_resource = jid_split(stanza.attr.to);
if user == nil then
return;
end
-- 1. Validate the resource part of the MUC JID (the occupant nickname).
if not muc_resource or not muc_resource:match(VALID_RESOURCE_PATTERN) then
module:log("warn", "Rejecting join with invalid MUC resource from %s (muc_resource: %s)",
stanza.attr.from, tostring(muc_resource));
origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Invalid resource"));
return true;
end
-- 2. Anonymous strict mode: MUC resource must equal the 8-char UUID prefix of the real JID username.
if anonymous_strict and is_anonymous_domain(domain) then
local uuid_prefix = user:sub(1, 8);
if muc_resource ~= uuid_prefix then
module:log("warn", "Rejecting anonymous user: MUC resource %s does not match UUID prefix %s (from %s)",
muc_resource, uuid_prefix, stanza.attr.from);
origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Invalid anonymous user ID"));
return true;
end
end
end
-- Returns true (halts event processing) when a MUC nick (resource) change should
-- be rejected. In Jitsi the occupant resource is the stable per-session identity
-- and must never change.
local function reject_nick_change(event)
local room, origin, stanza = event.room, event.origin, event.stanza;
if is_healthcheck_room(room.jid) then
return;
end
local orig_occupant = event.orig_occupant;
-- No original occupant means there is no rename to police here.
if not orig_occupant then
return;
end
local _, _, current_resource = jid_split(orig_occupant.nick);
local _, _, new_resource = jid_split(stanza.attr.to);
if new_resource ~= current_resource then
module:log("warn", "Rejecting MUC nick change from %s (%s -> %s)",
stanza.attr.from, tostring(current_resource), tostring(new_resource));
origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Nick changes are not allowed"));
return true;
end
end
module:log("info", "module loaded (anonymous_strict=%s)", tostring(anonymous_strict));
module:hook("muc-occupant-pre-join", check_resource, 11);
module:hook("muc-occupant-pre-change", reject_nick_change, 11);
module:hook_global("config-reloaded", load_config);