Skip to content

fix!: create inviteToken for invites and remove it from listInvites#38290

Open
julio-rocketchat wants to merge 11 commits intorelease-9.0.0from
refactor-invites
Open

fix!: create inviteToken for invites and remove it from listInvites#38290
julio-rocketchat wants to merge 11 commits intorelease-9.0.0from
refactor-invites

Conversation

@julio-rocketchat
Copy link
Copy Markdown
Member

@julio-rocketchat julio-rocketchat commented Jan 21, 2026

Proposed changes (including videos or screenshots)

Users who have the create-invite-links permission are also able to access /admin/invites and request all invites from the listInvites API endpoint. This means that users with such a permission are able to get invites for rooms they are not part of and they can use such invites to get arbitrary access to these rooms.

image

This happens because the invite ID works as a token and can be used in an invite link. Notwithstanding, the invite ID/token is too short and may be susceptible to brute-force attempts.

This PR:

  • Creates inviteToken and separates it from _id.
  • Doesn't return inviteToken in the listInvites response nor in the UI.
  • Increases _id's randomness and entropy with const _id = crypto.randomBytes(8).toString('hex');.
  • Generates inviteToken in a cryptographically secure manner by using const inviteToken = crypto.randomUUID();.

Issue(s)

VLN-38

Steps to test or reproduce

N/A

Further comments

N/A

Summary by CodeRabbit

  • API Changes

    • Created and returned inviteToken on invite creation; listings intentionally omit inviteToken.
    • Invite validation and usage now operate via inviteToken instead of internal IDs.
    • New APIs ensure invites include inviteToken for legacy records.
  • UI Updates

    • Renamed invites table header from "Token" to "Invite".
  • Tests

    • End-to-end tests updated to use and validate inviteToken, including edge-case checks.

@dionisio-bot
Copy link
Copy Markdown
Contributor

dionisio-bot Bot commented Jan 21, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is targeting the wrong base branch. It should target 9.0.0, but it targets 8.3.0

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 21, 2026

⚠️ No Changeset found

Latest commit: d3c2dff

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cccedcaf-4285-4cea-a18f-fa084c4391cc

📥 Commits

Reviewing files that changed from the base of the PR and between 3e5fff8 and d3c2dff.

📒 Files selected for processing (1)
  • apps/meteor/app/api/server/v1/invites.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/meteor/app/api/server/v1/invites.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build

Walkthrough

Adds an inviteToken used for public invite URLs and token-based lookups: models, server flows, APIs, client props/labels, and tests updated; legacy invites are backfilled with tokens; list API omits token from public listings while find/create returns it.

Changes

Cohort / File(s) Summary
API Response Schemas
apps/meteor/app/api/server/v1/invites.ts
listInvites 200 items changed to Omit<IInvite, 'inviteToken'>[] (inline object schema); findOrCreateInvite 200 response now includes required inviteToken: string.
Server Invite Functions
apps/meteor/app/invites/server/functions/findOrCreateInvite.ts, apps/meteor/app/invites/server/functions/listInvites.ts, apps/meteor/app/invites/server/functions/validateInviteToken.ts
Generate and ensure inviteToken via crypto.randomUUID() for new and legacy invites; use inviteToken in URL construction and lookups; validateInviteToken queries by token.
Client UI Components
apps/meteor/client/views/admin/invites/InviteRow.tsx, apps/meteor/client/views/admin/invites/InvitesPage.tsx
InviteRowProps now omits inviteToken; table header label changed from "Token" to "Invite".
End-to-End Tests
apps/meteor/tests/e2e/saml.spec.ts, apps/meteor/tests/end-to-end/api/abac.ts, apps/meteor/tests/end-to-end/api/invites.ts, apps/meteor/tests/end-to-end/api/users.ts
Tests updated to read inviteToken from responses, assert presence/absence as appropriate, store both _id and inviteToken where needed, and add negative cases for using _id as a token.
Type Definitions & Models
packages/core-typings/src/IInvite.ts, packages/model-typings/src/models/IInvitesModel.ts, packages/models/src/models/Invites.ts
Added inviteToken: string to IInvite; added findOneByInviteToken() and ensureInviteToken() to model typings and implemented them in InvitesRaw (uses crypto.randomUUID() and token backfill/ensure logic).

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API_Server as API Server
    participant Invites_DB as Invites DB

    Client->>API_Server: POST /findOrCreateInvite (roomId)
    activate API_Server
    API_Server->>Invites_DB: find existing invite by roomId
    alt invite not found
        API_Server->>API_Server: generate _id and inviteToken (crypto.randomUUID)
        API_Server->>Invites_DB: insert invite with inviteToken
    else invite found
        API_Server->>Invites_DB: ensureInviteToken(_id) -> token (generate if missing)
        API_Server->>Invites_DB: update invite with token if needed
    end
    API_Server->>API_Server: build invite URL using inviteToken
    API_Server-->>Client: return invite object including inviteToken and url
    deactivate API_Server

    Client->>API_Server: POST /validateInviteToken (inviteToken)
    activate API_Server
    API_Server->>Invites_DB: findOneByInviteToken(inviteToken)
    Invites_DB-->>API_Server: invite data / null
    API_Server-->>Client: return validation result
    deactivate API_Server
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: creating a new inviteToken field and removing it from the listInvites response.
Linked Issues check ✅ Passed The PR introduces inviteToken as a cryptographically secure identifier separate from _id and omits it from listInvites, directly addressing VLN-38's requirement to prevent users with create-invite-links from accessing invite links via enumeration.
Out of Scope Changes check ✅ Passed All changes align with the security objective: inviteToken introduction, removal from listInvites, test updates for token validation, UI label change for clarity, and increased _id entropy—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 21, 2026

📦 Docker Image Size Report

➡️ Changes

Service Current Baseline Change Percent
sum of all images 0B 0B 0B
account-service 0B 0B 0B
authorization-service 0B 0B 0B
ddp-streamer-service 0B 0B 0B
omnichannel-transcript-service 0B 0B 0B
presence-service 0B 0B 0B
queue-worker-service 0B 0B 0B
rocketchat 0B 0B 0B

📊 Historical Trend

---
config:
  theme: "dark"
  xyChart:
    width: 900
    height: 400
---
xychart
  title "Image Size Evolution by Service (Last 30 Days + This PR)"
  x-axis ["11/18 22:53", "11/19 23:02", "11/21 16:49", "11/24 17:34", "11/27 22:32", "11/28 19:05", "12/01 23:01", "12/02 21:57", "12/03 21:00", "12/04 18:17", "12/05 21:56", "12/08 20:15", "12/09 22:17", "12/10 23:26", "12/11 21:56", "12/12 22:45", "12/13 01:34", "12/15 22:31", "12/16 22:18", "12/17 21:04", "12/18 23:12", "12/19 23:27", "12/20 21:03", "12/22 18:54", "12/23 16:16", "12/24 19:38", "12/25 17:51", "12/26 13:18", "12/29 19:01", "12/30 20:52", "01/24 00:36 (PR)"]
  y-axis "Size (GB)" 0 --> 0.5
  line "account-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.00]
  line "authorization-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.00]
  line "ddp-streamer-service" [0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.00]
  line "omnichannel-transcript-service" [0.14, 0.14, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.00]
  line "presence-service" [0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.11, 0.00]
  line "queue-worker-service" [0.14, 0.14, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.13, 0.00]
  line "rocketchat" [0.35, 0.35, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.34, 0.00]
Loading

Statistics (last 30 days):

  • 📊 Average: 1.5GiB
  • ⬇️ Minimum: 1.4GiB
  • ⬆️ Maximum: 1.6GiB
  • 🎯 Current PR: 0B
ℹ️ About this report

This report compares Docker image sizes from this build against the develop baseline.

  • Tag: pr-38290
  • Baseline: develop
  • Timestamp: 2026-01-24 00:36:13 UTC
  • Historical data points: 30

Updated: Sat, 24 Jan 2026 00:36:14 GMT

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.88%. Comparing base (ff3a6fd) to head (d3c2dff).
⚠️ Report is 241 commits behind head on develop.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop   #38290      +/-   ##
===========================================
- Coverage    70.94%   70.88%   -0.06%     
===========================================
  Files         3207     3189      -18     
  Lines       113958   113311     -647     
  Branches     20694    20523     -171     
===========================================
- Hits         80842    80324     -518     
+ Misses       31059    30976      -83     
+ Partials      2057     2011      -46     
Flag Coverage Δ
unit 71.61% <ø> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@julio-rocketchat
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/meteor/app/invites/server/functions/validateInviteToken.ts (1)

6-21: Backwards-compat break for legacy invite links.

With token-only lookup, existing invite URLs based on _id will fail after deploy unless a migration/backfill updates tokens (and URLs) ahead of time or you add a legacy fallback lookup. Please confirm intended behavior and, if legacy links should keep working, add a compatibility path (e.g., fallback to _id and optionally backfill inviteToken/url).

apps/meteor/app/invites/server/functions/listInvites.ts (1)

1-25: Add explicit crypto import for consistency with sibling module.

The sibling file findOrCreateInvite.ts in the same module imports crypto from 'node:crypto'. For consistency, add the import here as well:

import type { IInvite } from '@rocket.chat/core-typings';
+import crypto from 'node:crypto';
import { Invites } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
🤖 Fix all issues with AI agents
In `@apps/meteor/app/invites/server/functions/findOrCreateInvite.ts`:
- Around line 93-103: The legacy invite token race is fixed by atomically
setting the token with a conditional findOneAndUpdate instead of
Invites.updateOne: when you detect an existing invite without inviteToken,
generate a token and call Invites.findOneAndUpdate({ _id: existing._id, $or: [{
inviteToken: { $exists: false } }, { inviteToken: null }, { inviteToken: "" }]
}, { $set: { inviteToken } }, { returnDocument: "after" }); then set existing to
the returned document (or, if null, re-fetch the invite) and compute
existing.url = getInviteUrl(existing) before returning; replace the current
updateOne + local assignment with this atomic pattern so the response token
always matches DB state.
🧹 Nitpick comments (3)
apps/meteor/app/invites/server/functions/listInvites.ts (1)

18-33: Batch legacy-token backfill and drop inline comments.

Consider batching updates to avoid no-await-in-loop, and remove inline comments to keep the implementation clean. This also speeds up token backfill when many invites exist.

♻️ Possible refactor (apply after fixing crypto import)
-	// Ensure all invites have inviteToken (for legacy invites that might not have it)
-	for (const invite of invites) {
-		const inviteWithToken = invite as IInvite & { inviteToken?: string };
-		if (!inviteWithToken.inviteToken) {
-			const inviteToken = randomUUID();
-			// eslint-disable-next-line no-await-in-loop
-			await Invites.updateOne({ _id: invite._id }, { $set: { inviteToken } });
-			inviteWithToken.inviteToken = inviteToken;
-		}
-	}
-
-	// Remove inviteToken from the response
-	return invites.map((invite) => {
+	const invitesNeedingToken = invites.filter((invite) => !(invite as IInvite & { inviteToken?: string }).inviteToken);
+	await Promise.all(
+		invitesNeedingToken.map(async (invite) => {
+			const inviteToken = randomUUID();
+			await Invites.updateOne({ _id: invite._id }, { $set: { inviteToken } });
+			(invite as IInvite & { inviteToken: string }).inviteToken = inviteToken;
+		}),
+	);
+
+	return invites.map((invite) => {
As per coding guidelines, avoid code comments in the implementation.
apps/meteor/tests/e2e/saml.spec.ts (1)

142-143: Consider renaming variable for clarity.

The variable inviteId now holds the inviteToken value rather than the _id. While functionally correct (invite URLs should use the token), the naming is misleading.

Consider renaming to better reflect its purpose:

♻️ Suggested variable rename
-		const { inviteToken } = await inviteResponse.json();
-		inviteId = inviteToken;
+		const { inviteToken: token } = await inviteResponse.json();
+		inviteId = token;

Or rename the variable declaration at line 100:

-	let inviteId: string;
+	let inviteToken: string;

And update all usages of inviteId to inviteToken throughout the file (lines 445, 448, 459, 462, 485, 488, 508, 511).

apps/meteor/tests/end-to-end/api/invites.ts (1)

104-112: Stabilize listInvites assertion by matching on ID (avoid order dependence).

The list order isn’t guaranteed; picking index 0 can make the test flaky if other invites exist. Consider locating the invite by _id before asserting on inviteToken.

♻️ Proposed adjustment
-					expect(res.body[0]).to.have.property('_id', testInviteID);
-					expect(res.body[0]).to.not.have.property('inviteToken');
+					const invite = res.body.find((i: IInvite) => i._id === testInviteID);
+					expect(invite).to.exist;
+					expect(invite).to.not.have.property('inviteToken');

Comment thread apps/meteor/app/invites/server/functions/findOrCreateInvite.ts
@julio-rocketchat julio-rocketchat marked this pull request as ready for review January 22, 2026 13:43
@julio-rocketchat julio-rocketchat requested review from a team as code owners January 22, 2026 13:43
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 13 files

Copy link
Copy Markdown
Contributor

@pierre-lehnen-rc pierre-lehnen-rc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change breaks compatibility with all existing Invite URLs, which IMO would already be a bad idea in a major version, and is even more so in a minor one.

Comment thread apps/meteor/app/invites/server/functions/findOrCreateInvite.ts Outdated
@julio-rocketchat
Copy link
Copy Markdown
Member Author

This change breaks compatibility with all existing Invite URLs, which IMO would already be a bad idea in a major version, and is even more so in a minor one.

What would be a better alternative in your opinion to ensure security at the same time that we don't break compatibility?

@pierre-lehnen-rc
Copy link
Copy Markdown
Contributor

It wouldn't have the same level of security, but one option would be to update the old invites setting their old ID as the inviteToken (and perhaps changing their ID to a different value); That way the old URLs would still work and would not be leaked anymore.

Then in addition we could perhaps make the invite list show an indicator on old invites suggesting they should be replaced.

@pierre-lehnen-rc
Copy link
Copy Markdown
Contributor

BTW the old token length (6) was a product decision. It was shorter in order to make it easier for users to type them out if needed (though I don't know if we have any workflow in which the code is typed manually).

@julio-rocketchat
Copy link
Copy Markdown
Member Author

It wouldn't have the same level of security, but one option would be to update the old invites setting their old ID as the inviteToken (and perhaps changing their ID to a different value); That way the old URLs would still work and would not be leaked anymore.

Then in addition we could perhaps make the invite list show an indicator on old invites suggesting they should be replaced.

I see. I'll talk to Product about it and take a look into doing it this way as well.

@julio-rocketchat
Copy link
Copy Markdown
Member Author

BTW the old token length (6) was a product decision. It was shorter in order to make it easier for users to type them out if needed (though I don't know if we have any workflow in which the code is typed manually).

Got it. I'll talk to product about this one as well, but I don't think we have workflows where the code is typed manually either.

@julio-rocketchat julio-rocketchat changed the title fix: create inviteToken for invites and remove it from listInvites fix!: create inviteToken for invites and remove it from listInvites Jan 22, 2026
@julio-rocketchat
Copy link
Copy Markdown
Member Author

@pierre-lehnen-rc talked to Milton about this one and he agreed to wait for 9.0.0 to do this fix.

@sampaiodiego
Copy link
Copy Markdown
Member

This means that users with such a permission are able to get invites for rooms they are not part of and they can use such invites to get arbitrary access to these rooms.

@julio-rocketchat is this really a thing? I don't think so by the looking at the code.. if that's not true does it means there is no vulnerability at all?

@julio-rocketchat
Copy link
Copy Markdown
Member Author

This means that users with such a permission are able to get invites for rooms they are not part of and they can use such invites to get arbitrary access to these rooms.

@julio-rocketchat is this really a thing? I don't think so by the looking at the code.. if that's not true does it means there is no vulnerability at all?

@sampaiodiego

I’ve created a few private rooms with the rocketchat.internal.admin.test user and created invite links for each. Then, with the user julio, who only has the create-invite-links permission, I hit the listInvites endpoint and got the following:

image

With these, since _id is the token, we can form invite links such as https://go.rocket.chat/invite?host=localhost%3A3000&path=invite%2FRtgCeb&secure=no.

Then, I used it to get access to the private rooms with the user julio:

image

@sampaiodiego
Copy link
Copy Markdown
Member

sampaiodiego commented Jan 26, 2026

@julio-rocketchat I assume then you have created a new role with create-invite-links permission, right? this permission is "scoped", which means it should require a room id to be validated (thus default roles being owner and moderator - and admin as well), so with default roles the issue doesn't happen. I'll check if there is something we can do with the permission validation system as a whole, like not allow scoped permission being assigned to not scoped roles..

@julio-rocketchat
Copy link
Copy Markdown
Member Author

julio-rocketchat commented Jan 26, 2026

@julio-rocketchat I assume then you have created a new role with create-invite-links permission, right? this permission is "scoped", which means it should require a room id to be validated (thus default roles being owner and moderator - and admin as well), so with default roles the issue doesn't happen. I'll check if there is something we can do with the permission validation system as a whole, like not allow scoped permission being assigned to not scoped roles..

@sampaiodiego for the PoC I gave the user role that permission. It would probably work with newly created roles as well, but haven't tested it yet.

I've thought about creating a new manage-invite-links but that would require migrations and I'm not sure it would be less of a breaking change than this option - and I also think that even if we had a separate permission, increasing the token complexity, making sure it's cryptographically random and separating ID and token are security improvements overall

@julio-rocketchat julio-rocketchat added this to the 9.0.0 milestone May 5, 2026
@julio-rocketchat julio-rocketchat changed the base branch from develop to release-9.0.0 May 5, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants