Regex room-claim case mutation permits unauthorized protected-meeting access
Lowercasing a signed Lua pattern changed the room policy after JWT verification
CVSS 3.1 base score
HighVector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Summary
Source
init_session()
resources/prosody-plugins/mod_jitsi_session.lua:32
Sink
Util:verify_room()
resources/prosody-plugins/token/util.lib.lua:457
A participant with a legitimate regex-scoped JWT could join a protected room that the issuer's signed pattern did not authorize. A claim such as ^team-%D+$ was transformed into ^team-%d+$, turning a non-digit policy into a digit policy before the MUC join check.
Root cause. Util:verify_room() lowercased the room claim before deciding whether it was a literal room or a Lua pattern. Case conversion is safe for a canonical room identifier, but it mutates pattern operators such as %D, %A, %S, and %W into operators with different meanings.
Remediation evidence. Jitsi opened PR #17803 the day after the disclosure and merged it on 10 September 2026. The patch preserves regex claims byte-for-byte, canonicalizes literal claims separately, requires a full-string match, and adds paired uppercase/lowercase pattern regression tests.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- None
- ScopeS
- Unchanged
- ConfidentialityC
- High
- IntegrityI
- Low
- AvailabilityA
- None
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlledresources/
prosody-plugins/ mod_jitsi_session.lua:32 init_session()
- Step 01resources/
prosody-plugins/ mod_jitsi_session.lua:32 The public BOSH/WebSocket session accepts a valid bearer JWT and stores it on the authoritative Prosody session.
python if request.headers["authorization"] then token = request.headers["authorization"]:sub(8) end session.auth_token = token - Step 02resources/
prosody-plugins/ token/ util.lib.lua:335 Signature, issuer, and audience verification succeeds, after which the signed room expression and regex marker become session policy.
python local claims = jwt.verify(session.auth_token, algorithm, key, issuers, audiences) session.jitsi_meet_room = claims["room"] session.jitsi_meet_context_room = claims["context"]["room"] - Step 03resources/
prosody-plugins/ token/ util.lib.lua:457 The vulnerable verifier lowercased pattern syntax, so the resulting expression authorized a different set of rooms than the issuer signed.
python local auth_room = string.lower(session.jitsi_meet_room) local result = string.match(room_node, auth_room) - Sinkresources/
prosody-plugins/ token/ util.lib.lua:457 Util:verify_room()
Impact
Reported impact
The attacker crosses the issuer-defined room boundary and joins an existing protected meeting as a participant, gaining its chat, signaling, and media participation path. The finding does not claim moderator privileges or a service-wide outage.
Attack surface
Public Jitsi BOSH or XMPP WebSocket authentication and MUC join handling in deployments that enable token verification and regex room claims.
Preconditions
The attacker needs an ordinary valid participant JWT containing an affected uppercase Lua pattern class, and a same-tenant room that matches the lowercased pattern but not the signed pattern. No signing key or moderator credential is required.
Attack path
- 1
Obtain a legitimate token scoped to a regex such as
^team-%D+$. - 2
Authenticate normally over BOSH or WebSocket with that token.
- 3
Join an existing room such as
team-123, which the original signed pattern excludes. - 4
The verifier lowercases
%Dto%dand admits the participant to the protected room.
Proof of concept
- 01
Environment setup
Run an isolated token-authenticated Jitsi deployment on a vulnerable revision and create the protected room
team-123. - 02
Target configuration
Issue an ordinary participant JWT with
room: "^team-%D+$"andcontext.room.regex: true. The token must otherwise have a valid signature, issuer, audience, and tenant scope. - 03
Exploit delivery
Authenticate using the legitimate token, then send a normal MUC join presence for
team-123@conference.example.test/attacker. - 04
Expected response
The vulnerable server returns successful self-presence and conference signaling even though
team-123does not match the signed%Dpolicy. - 05
Outcome
PR #17803 keeps regex text unchanged and requires a complete match, so the same join is rejected while genuinely matching rooms remain accessible.
Remediation
Guidance
Upgrade to a Jitsi Meet release containing PR #17803. Preserve signed regex text exactly, lowercase only literal room identifiers, validate pattern syntax, and require a full-resource match.
local auth_room = string.lower(session.jitsi_meet_room)local result = string.match(room_node, auth_room)local regex_room = context_room and context_room.regex == trueif not regex_room then auth_room = string.lower(auth_room) endlocal first, last = string.find(room_node, auth_room)if first ~= 1 or last ~= #room_node then return false end
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.
