Filed publicly because this repository has no SECURITY.md and private
vulnerability reporting is not enabled, so there is no private channel to
use. Happy to move this somewhere else if you would prefer.
Affects only deployments that set JITSI_JWT_SECRET — without it the
sidecar returns a plain URL and none of this applies.
Summary
The jitsi-meet plugin's sidecar appends the signed Jitsi JWT to the meeting URL, and that same
URL is written to the calendar event as the meeting link. Calendar invites are sent to every
attendee, so every attendee receives a token minted for the organiser. Anyone holding the
invite can join carrying the organiser's identity, for the lifetime of the token.
Where
jitsi-meet/server/index.js:
const roomName = generateRoomName(eventTitle);
let url = buildMeetingUrl(JITSI_URL, roomName);
if (JITSI_JWT_SECRET) {
const token = await createJitsiJwt({
secret: JITSI_JWT_SECRET,
roomName,
userEmail: claims.email || req.headers['x-jmap-username'] || '',
userName: claims.name || '',
jitsiUrl: JITSI_URL,
});
url += `?jwt=${token}`; // <-- goes into the invite
}
return sendJson(res, 200, { url });
The client then sets that URL as the event's meeting link (setVirtualLocation(url)), which is
what attendees receive.
What the token grants
From jitsi-meet/server/lib/jitsi.js:
const payload = {
iss: 'bulwark-webmail',
sub: domain,
aud: 'jitsi',
room: roomName,
iat: now,
exp: now + 86400, // 24 hours
context: { user: { name: userName, email: userEmail } },
};
- Identity.
context.user carries the organiser's name and email. Any recipient opening the
link joins displaying the organiser's identity. Other participants have no way to tell them
apart from the real organiser.
- Moderator. With a JWT-authenticated Prosody vhost, a token holder is an authenticated
participant. Jicofo's enable-auto-owner defaults to true, so the first authenticated
participant to join becomes moderator — frequently an attendee rather than the organiser.
Moderator can kick and mute participants, and on many deployments start recording.
- Duration. 24 hours, and the token cannot be revoked once distributed.
room: roomName does scope the token to the single room, which bounds the impact — this is not a
whole-server compromise. Within that meeting, however, it is impersonation plus privilege
transfer to everyone on the invite, including forwarded copies and anyone with mailbox access.
Reproduce
- Run the sidecar with
JITSI_JWT_SECRET set and a Jitsi instance using JWT auth.
- Create a calendar event, click Add Jitsi Meeting, invite a second address.
- Read the invite as the attendee: the meeting link contains
?jwt=….
- Decode the payload — it holds the organiser's name and email.
- Open the link as the attendee before the organiser joins: you are authenticated, shown as the
organiser, and made moderator.
Suggested fix
The organiser's credential should never be an attachment to a link that gets forwarded. Options,
roughly in order of preference:
- Return the plain room URL for the event, and mint the token at join time. Give the plugin
a separate "Start meeting" action that requests a fresh, short-lived token for the current
user. The invite then carries only https://meet.example.com/<room>.
- Put a launcher in front of the room. The event link points at your own origin; the
launcher grants a token only to an authenticated user and sends everyone else to the room as a
guest. One link is then safe to share, and host rights follow identity rather than possession
of the URL.
- If the token must stay in the URL, at minimum cut
exp to minutes rather than 24 hours, drop
context.user, and set context.user.moderator explicitly rather than relying on
enable-auto-owner to pick whoever arrives first.
Option 2 is what we deployed locally, because it keeps the single-link UX that makes the plugin
pleasant to use while removing the credential from the invite entirely.
Related
The README states tokens are verified via JWKS, but server/lib/oidc.js verifies by calling the
issuer's userinfo_endpoint instead. That is a defensible design, but it is a network round trip
per request and a different trust model than documented — worth correcting in the docs.
Summary
The
jitsi-meetplugin's sidecar appends the signed Jitsi JWT to the meeting URL, and that sameURL is written to the calendar event as the meeting link. Calendar invites are sent to every
attendee, so every attendee receives a token minted for the organiser. Anyone holding the
invite can join carrying the organiser's identity, for the lifetime of the token.
Where
jitsi-meet/server/index.js:The client then sets that URL as the event's meeting link (
setVirtualLocation(url)), which iswhat attendees receive.
What the token grants
From
jitsi-meet/server/lib/jitsi.js:context.usercarries the organiser's name and email. Any recipient opening thelink joins displaying the organiser's identity. Other participants have no way to tell them
apart from the real organiser.
participant. Jicofo's
enable-auto-ownerdefaults totrue, so the first authenticatedparticipant to join becomes moderator — frequently an attendee rather than the organiser.
Moderator can kick and mute participants, and on many deployments start recording.
room: roomNamedoes scope the token to the single room, which bounds the impact — this is not awhole-server compromise. Within that meeting, however, it is impersonation plus privilege
transfer to everyone on the invite, including forwarded copies and anyone with mailbox access.
Reproduce
JITSI_JWT_SECRETset and a Jitsi instance using JWT auth.?jwt=….organiser, and made moderator.
Suggested fix
The organiser's credential should never be an attachment to a link that gets forwarded. Options,
roughly in order of preference:
a separate "Start meeting" action that requests a fresh, short-lived token for the current
user. The invite then carries only
https://meet.example.com/<room>.launcher grants a token only to an authenticated user and sends everyone else to the room as a
guest. One link is then safe to share, and host rights follow identity rather than possession
of the URL.
expto minutes rather than 24 hours, dropcontext.user, and setcontext.user.moderatorexplicitly rather than relying onenable-auto-ownerto pick whoever arrives first.Option 2 is what we deployed locally, because it keeps the single-link UX that makes the plugin
pleasant to use while removing the credential from the invite entirely.
Related
The README states tokens are verified via JWKS, but
server/lib/oidc.jsverifies by calling theissuer's
userinfo_endpointinstead. That is a defensible design, but it is a network round tripper request and a different trust model than documented — worth correcting in the docs.