Skip to content

jitsi-meet: the moderator JWT is put in the meeting link sent to every attendee #12

Description

@Tarek-Elmoursi

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 } },
};
  1. 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.
  2. 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.
  3. 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

  1. Run the sidecar with JITSI_JWT_SECRET set and a Jitsi instance using JWT auth.
  2. Create a calendar event, click Add Jitsi Meeting, invite a second address.
  3. Read the invite as the attendee: the meeting link contains ?jwt=….
  4. Decode the payload — it holds the organiser's name and email.
  5. 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:

  1. 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>.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions