Skip to content

regression: 2fa inline errors for API calls - #40678

Closed
gabriellsh wants to merge 9 commits into
release-8.5.0from
refactor/totpApi
Closed

regression: 2fa inline errors for API calls#40678
gabriellsh wants to merge 9 commits into
release-8.5.0from
refactor/totpApi

Conversation

@gabriellsh

@gabriellsh gabriellsh commented May 25, 2026

Copy link
Copy Markdown
Member

Proposed changes (including videos or screenshots)

Issue(s)

Steps to test or reproduce

Further comments

Summary by CodeRabbit

  • New Features

    • Password-based 2FA now securely hashes the password before verification.
  • Bug Fixes

    • Improved detection and handling of two-factor errors, including max-attempts cases and clearer retry behavior.
    • Better modal lifecycle handling to avoid stale or conflicting 2FA prompts.
  • Refactor

    • Centralized and simplified two-factor challenge flow for more consistent retry and retry-resolve behavior.
  • Tests

    • Added comprehensive tests covering 2FA dialogs, retries, cancellations, and hashing behavior.

Review Change Stack

@gabriellsh
gabriellsh requested a review from a team as a code owner May 25, 2026 17:08
@dionisio-bot

dionisio-bot Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

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 missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot

changeset-bot Bot commented May 25, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6be1d19

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

@gabriellsh
gabriellsh changed the base branch from develop to release-8.5.0 May 25, 2026 17:08
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors 2FA handling to an error-driven callback pattern: centralizes 2FA error predicates, adds a promise-based client handler (challenge2fa) that opens TwoFactorModal, and updates RestClient to invoke the handler on parsed HTTP 400 errors and retry with x-2fa headers.

Changes

Two-Factor Authentication Challenge Flow Refactor

Layer / File(s) Summary
2FA error types and predicates
packages/api-client/src/errors.ts
Defines TwoFactorMethod and TwoFactorErrorResponse, adds shared isTotpError guard, reimplements isTotpRequiredError/isTotpInvalidError, and adds isTotpMaxAttemptsError.
RestClient 2FA challenge callback mechanism
packages/api-client/src/RestClientInterface.ts, packages/api-client/src/index.ts
Updates handleTwoFactorChallenge signature to accept { error }-driven callback returning `[Promise, resolveChallenge]
Challenge2fa handler implementation
apps/meteor/client/lib/2fa/challenge2fa.ts
New module that creates externally-resolvable code and lifecycle promises, maintains unresolvedChallenge callbacks across retries, derives modal props (including email/username), handles terminal errors (max-attempts), supports retry semantics, and returns [codePromise, endFn].
Client modal integration, utils, and tests
apps/meteor/client/lib/2fa/process2faReturn.ts, apps/meteor/client/lib/2fa/utils.ts, apps/meteor/client/lib/2fa/challenge2fa.spec.tsx
Narrows process2faAsyncReturn error type to `MeteorErrorLike
Final handler registration
apps/meteor/app/utils/client/lib/RestApiClient.ts
Replace previous modal-based registration with APIClient.handleTwoFactorChallenge(challenge2fa).

Sequence Diagram

sequenceDiagram
  participant Client
  participant RestClient
  participant Errors
  participant Challenge2fa
  participant TwoFactorModal
  Client->>RestClient: send(request)
  RestClient->>RestClient: receives HTTP 400
  RestClient->>Errors: parse JSON error
  RestClient->>Challenge2fa: challenge2fa({ error })
  Challenge2fa->>TwoFactorModal: open modal with handlers
  TwoFactorModal->>Challenge2fa: onConfirm(code)
  Challenge2fa->>Challenge2fa: hash code if password
  Challenge2fa-->>RestClient: resolve code promise
  RestClient->>RestClient: retry request with x-2fa-code/x-2fa-method
  RestClient->>Challenge2fa: resolveChallenge()
  RestClient-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • KevLehman
  • tassoevan
  • ricardogarim
🚥 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 accurately reflects the main change: refactoring 2FA handling in API calls to show inline errors instead of using the previous modal-based approach.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/meteor/client/lib/2fa/utils.ts (1)

3-9: 💤 Low value

Consider re-exporting isTotpMaxAttemptsError from @rocket.chat/api-client as well.

Since isTotpInvalidError and isTotpRequiredError are now re-exported from @rocket.chat/api-client, and isTotpMaxAttemptsError is also exported from the same package (see packages/api-client/src/errors.ts line 35 and the export * from './errors' in the package index), the local implementation here is now duplicative.

Suggested change
 import type { MeteorErrorLike } from './types';

-export { isTotpInvalidError, isTotpRequiredError } from '`@rocket.chat/api-client`';
-
-export const isTotpMaxAttemptsError = (
-	error: unknown,
-): error is MeteorErrorLike & ({ error: 'totp-max-attempts' } | { errorType: 'totp-max-attempts' }) =>
-	(error as { error?: unknown } | undefined)?.error === 'totp-max-attempts' ||
-	(error as { errorType?: unknown } | undefined)?.errorType === 'totp-max-attempts';
+export { isTotpInvalidError, isTotpRequiredError, isTotpMaxAttemptsError } from '`@rocket.chat/api-client`';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/2fa/utils.ts` around lines 3 - 9, The file defines a
local isTotpMaxAttemptsError but other errors (isTotpInvalidError,
isTotpRequiredError) are already re-exported from `@rocket.chat/api-client`;
remove the local isTotpMaxAttemptsError implementation and replace it with a
re-export from the same package so all three come from `@rocket.chat/api-client`
(match the existing export style used for isTotpInvalidError and
isTotpRequiredError); ensure you update exports in
apps/meteor/client/lib/2fa/utils.ts to export isTotpMaxAttemptsError from
`@rocket.chat/api-client` and delete the local type-guard implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api-client/src/errors.ts`:
- Around line 16-30: isTotpError currently promises a full
TwoFactorError<TErrorType> (including details.method) but only checks top-level
error/errorType fields; update the predicate so its type matches the runtime
checks instead of claiming details exist — e.g., change the return type of
isTotpError to assert only that the value has error === TErrorType or errorType
=== TErrorType (a narrower type such as a union of objects with just error or
errorType) and do not claim TwoFactorError<TErrorType> (or require
TwoFactorMethod); update references to isTotpError and any call sites that rely
on details.method to first run hasRequiredTwoFactorMethod or narrow the type
before accessing details.method.

---

Nitpick comments:
In `@apps/meteor/client/lib/2fa/utils.ts`:
- Around line 3-9: The file defines a local isTotpMaxAttemptsError but other
errors (isTotpInvalidError, isTotpRequiredError) are already re-exported from
`@rocket.chat/api-client`; remove the local isTotpMaxAttemptsError implementation
and replace it with a re-export from the same package so all three come from
`@rocket.chat/api-client` (match the existing export style used for
isTotpInvalidError and isTotpRequiredError); ensure you update exports in
apps/meteor/client/lib/2fa/utils.ts to export isTotpMaxAttemptsError from
`@rocket.chat/api-client` and delete the local type-guard implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89516109-87ed-414a-96c8-2cad9c14c724

📥 Commits

Reviewing files that changed from the base of the PR and between 116c088 and 068699b.

📒 Files selected for processing (7)
  • apps/meteor/app/utils/client/lib/RestApiClient.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/utils.ts
  • packages/api-client/src/RestClientInterface.ts
  • packages/api-client/src/errors.ts
  • packages/api-client/src/index.ts
📜 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). (5)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: CodeQL-Build
  • GitHub Check: Hacktron Security Check
  • GitHub Check: CodeQL-Build
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/client/lib/2fa/utils.ts
  • apps/meteor/app/utils/client/lib/RestApiClient.ts
  • packages/api-client/src/RestClientInterface.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/index.ts
  • packages/api-client/src/errors.ts
🧠 Learnings (5)
📚 Learning: 2026-02-10T16:32:42.586Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 38528
File: apps/meteor/client/startup/roles.ts:14-14
Timestamp: 2026-02-10T16:32:42.586Z
Learning: In Rocket.Chat's Meteor client code, DDP streams use EJSON and Date fields arrive as Date objects; do not manually construct new Date() in stream handlers (for example, in sdk.stream()). Only REST API responses return plain JSON where dates are strings, so implement explicit conversion there if needed. Apply this guidance to all TypeScript files under apps/meteor/client to ensure consistent date handling in DDP streams and REST responses.

Applied to files:

  • apps/meteor/client/lib/2fa/utils.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
📚 Learning: 2026-05-11T20:30:35.265Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 40480
File: apps/meteor/client/meteor/startup/accounts.ts:59-61
Timestamp: 2026-05-11T20:30:35.265Z
Learning: In Rocket.Chat’s Meteor client code, when calling `dispatchToastMessage` with `{ type: 'error' }`, pass the raw caught error object as `message` without manual normalization. `dispatchToastMessage` is designed to accept `message: unknown` for error toasts, so avoid converting errors to strings (e.g., `String(error)`) or extracting `error.message` before passing them.

Applied to files:

  • apps/meteor/client/lib/2fa/utils.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In the Rocket.Chat repository, do not reference Biome lint rules in code review feedback. Biome is not used even if biome.json exists; only reference Biome rules if there is explicit, project-wide usage documented. For TypeScript files, review lint implications without Biome guidance unless the project enables Biome rules.

Applied to files:

  • apps/meteor/client/lib/2fa/utils.ts
  • apps/meteor/app/utils/client/lib/RestApiClient.ts
  • packages/api-client/src/RestClientInterface.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/index.ts
  • packages/api-client/src/errors.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In this repository (RocketChat/Rocket.Chat), Biome lint rules are not used even if a biome.json exists. When reviewing TypeScript files (e.g., packages/ui-voip/src/providers/useMediaSession.ts), ensure lint suggestions do not reference Biome-specific rules. Rely on general ESLint/TypeScript lint rules and project conventions instead.

Applied to files:

  • apps/meteor/client/lib/2fa/utils.ts
  • apps/meteor/app/utils/client/lib/RestApiClient.ts
  • packages/api-client/src/RestClientInterface.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/index.ts
  • packages/api-client/src/errors.ts
📚 Learning: 2026-05-06T12:21:44.083Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 40256
File: apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx:121-149
Timestamp: 2026-05-06T12:21:44.083Z
Learning: Field wrappers in rocket.chat/fuselage-forms (Field, FieldLabel, FieldRow, FieldError, FieldHint) auto-create htmlFor/id associations, aria-describedby, and role="alert" for errors. Do not manually set htmlFor, id, aria-describedby, or role attributes when using these wrappers. This automatic wiring does not apply to plain rocket.chat/fuselage components, which require explicit ID wiring per the accessibility docs. In code reviews, prefer using fuselage-forms wrappers for form fields and verify there is no unnecessary manual ID/aria wiring in files that use these wrappers. If a component uses plain fuselage components, ensure proper id wiring as per docs.

Applied to files:

  • apps/meteor/client/lib/2fa/utils.ts
  • apps/meteor/app/utils/client/lib/RestApiClient.ts
  • packages/api-client/src/RestClientInterface.ts
  • apps/meteor/client/lib/2fa/process2faReturn.ts
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/index.ts
  • packages/api-client/src/errors.ts
🧬 Code graph analysis (4)
apps/meteor/app/utils/client/lib/RestApiClient.ts (1)
apps/meteor/client/lib/2fa/challenge2fa.ts (1)
  • challenge2fa (89-161)
apps/meteor/client/lib/2fa/process2faReturn.ts (1)
apps/meteor/client/lib/2fa/types.ts (1)
  • MeteorErrorLike (4-9)
apps/meteor/client/lib/2fa/challenge2fa.ts (4)
packages/api-client/src/errors.ts (5)
  • TwoFactorMethod (3-3)
  • isTotpMaxAttemptsError (36-36)
  • isTotpRequiredError (32-32)
  • isTotpInvalidError (34-34)
  • hasRequiredTwoFactorMethod (40-48)
apps/meteor/client/lib/user.ts (1)
  • getUser (13-17)
apps/meteor/client/components/TwoFactorModal/TwoFactorModal.tsx (1)
  • TwoFactorModal (28-44)
apps/meteor/client/lib/toast.ts (1)
  • dispatchToastMessage (22-25)
packages/api-client/src/index.ts (1)
apps/meteor/client/lib/2fa/challenge2fa.ts (1)
  • challenge2fa (89-161)
🔇 Additional comments (12)
packages/api-client/src/RestClientInterface.ts (1)

89-91: LGTM!

packages/api-client/src/index.ts (3)

16-16: LGTM!

Also applies to: 54-54


222-250: LGTM!


303-305: LGTM!

apps/meteor/client/lib/2fa/challenge2fa.ts (4)

1-18: LGTM!


23-56: LGTM!


58-82: LGTM!


89-161: LGTM!

apps/meteor/client/lib/2fa/process2faReturn.ts (2)

91-91: LGTM!


140-147: LGTM!

apps/meteor/app/utils/client/lib/RestApiClient.ts (1)

4-4: LGTM!

Also applies to: 31-31

packages/api-client/src/errors.ts (1)

3-15: LGTM!

Also applies to: 32-37, 41-47

Comment thread packages/api-client/src/errors.ts

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/api-client/src/index.ts">

<violation number="1" location="packages/api-client/src/index.ts:222">
P1: Guard `error` before using `'details' in error`; this line can throw a TypeError for non-object 400 payloads.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

'x-2fa-method': method2fa,
},
});
const method2fa = 'details' in error ? error.details.method : 'password';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Guard error before using 'details' in error; this line can throw a TypeError for non-object 400 payloads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/api-client/src/index.ts, line 222:

<comment>Guard `error` before using `'details' in error`; this line can throw a TypeError for non-object 400 payloads.</comment>

<file context>
@@ -223,28 +219,35 @@ export class RestClient implements RestClientInterface {
-						'x-2fa-method': method2fa,
-					},
-				});
+			const method2fa = 'details' in error ? error.details.method : 'password';
+
+			if (!this.challenge2fa) {
</file context>
Suggested change
const method2fa = 'details' in error ? error.details.method : 'password';
const method2fa =
typeof error === 'object' &&
error !== null &&
'details' in error &&
typeof (error as { details?: { method?: unknown } }).details?.method === 'string'
? ((error as { details: { method: 'totp' | 'email' | 'password' } }).details.method as 'totp' | 'email' | 'password')
: 'password';

Comment thread apps/meteor/client/lib/2fa/challenge2fa.ts
Comment thread packages/api-client/src/errors.ts
@hacktron-app

hacktron-app Bot commented May 25, 2026

Copy link
Copy Markdown

🟡 Medium: Automatic Email Verification Bypass during OAuth User Merge

In the BeforeUpdateOrCreateUserFromExternalService hook within CustomOAuth, when a user is merged from an external OAuth service, the system automatically marks the user's email address as verified (verified: true) if the keyField is set to 'username'. Since the OAuth identity data is controlled by the external identity provider (or a malicious actor if they can influence the provider's response), this allows an attacker to bypass email verification for arbitrary accounts if they can successfully trigger a merge with an existing account.

Steps to Reproduce
  1. Configure a Custom OAuth provider with keyField set to 'username' and mergeUsers set to true.
  2. An attacker creates an account on the Rocket.Chat instance with an unverified email address.
  3. The attacker creates an account on the configured OAuth provider with the same username and the target email address.
  4. The attacker logs into Rocket.Chat using the OAuth provider.
  5. The system merges the OAuth identity with the existing account based on the username and automatically sets the email address to verified: true.
Trace
graph TD
    subgraph SG0 ["./Rocket.Chat/apps/meteor/app/custom-oauth/server/custom_oauth_server.js"]
        ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js{{"Initializes and configures custom OAuth service providers, handling identity fetching and user account linking."}}
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["./Rocket.Chat/apps/meteor/app/dolphin/server/lib.ts"]
        ._Rocket.Chat_apps_meteor_app_dolphin_server_lib.ts["Defines the Dolphin OAuth integration setup and configuration."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["./Rocket.Chat/apps/meteor/app/drupal/server/lib.ts"]
        ._Rocket.Chat_apps_meteor_app_drupal_server_lib.ts["Defines the Drupal OAuth integration setup."]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["./Rocket.Chat/apps/meteor/app/gitlab/server/lib.ts"]
        ._Rocket.Chat_apps_meteor_app_gitlab_server_lib.ts["Module-level initialization for Gitlab OAuth integration."]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["./Rocket.Chat/apps/meteor/app/nextcloud/server/lib.ts"]
        ._Rocket.Chat_apps_meteor_app_nextcloud_server_lib.ts["Configures Nextcloud OAuth integration."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["./Rocket.Chat/apps/meteor/app/wordpress/server/lib.ts"]
        ._Rocket.Chat_apps_meteor_app_wordpress_server_lib.ts["Top-level module initialization for WordPress OAuth integration, including configuration setup and settings watching."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["./Rocket.Chat/apps/meteor/client/lib/e2ee/logger.ts"]
        ._Rocket.Chat_apps_meteor_client_lib_e2ee_logger.ts["Initializes global logging configurations and defines the Logger and Span classes."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["./Rocket.Chat/apps/meteor/server/lib/oauth/updateOAuthServices.ts"]
        updateOAuthServices["Updates OAuth service configurations based on application settings."]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js --> ._Rocket.Chat_apps_meteor_client_lib_e2ee_logger.ts
    ._Rocket.Chat_apps_meteor_app_wordpress_server_lib.ts --> ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js
    updateOAuthServices --> ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js
    ._Rocket.Chat_apps_meteor_app_dolphin_server_lib.ts --> ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js
    ._Rocket.Chat_apps_meteor_app_drupal_server_lib.ts --> ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js
    ._Rocket.Chat_apps_meteor_app_gitlab_server_lib.ts --> ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js
    ._Rocket.Chat_apps_meteor_app_nextcloud_server_lib.ts --> ._Rocket.Chat_apps_meteor_app_custom-oauth_server_custom_oauth_server.js
Loading
Fix with AI

Open in Cursor Open in Claude

Fix the following security vulnerability found by Hacktron.

File: apps/meteor/app/wordpress/server/lib.ts
Severity: medium

Vulnerability: Automatic Email Verification Bypass during OAuth User Merge

Description:
In the `BeforeUpdateOrCreateUserFromExternalService` hook within `CustomOAuth`, when a user is merged from an external OAuth service, the system automatically marks the user's email address as verified (`verified: true`) if the `keyField` is set to 'username'. Since the OAuth identity data is controlled by the external identity provider (or a malicious actor if they can influence the provider's response), this allows an attacker to bypass email verification for arbitrary accounts if they can successfully trigger a merge with an existing account.

Proof of Concept:
**Steps to Reproduce**

1. Configure a Custom OAuth provider with `keyField` set to 'username' and `mergeUsers` set to true.
2. An attacker creates an account on the Rocket.Chat instance with an unverified email address.
3. The attacker creates an account on the configured OAuth provider with the same username and the target email address.
4. The attacker logs into Rocket.Chat using the OAuth provider.
5. The system merges the OAuth identity with the existing account based on the username and automatically sets the email address to `verified: true`.

Affected Code:
if (this.keyField === 'username' && serviceData.email) {
	updater.set('emails', [{ address: serviceData.email, verified: true }]);
}

Fix this vulnerability. Only change what's necessary - don't modify unrelated code.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), or !accepted_risk <reason>. Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing. Any other reply is saved as a triage note.

View finding in Hacktron

@hacktron-app

hacktron-app Bot commented May 25, 2026

Copy link
Copy Markdown

🟡 Medium: Missing Re-authentication for 2FA Enablement

The users.2fa.enableEmail endpoint allows an authenticated user to enable email-based 2FA without requiring password re-authentication or a secondary 2FA challenge. In contrast, disabling email 2FA (users.2fa.disableEmail) and other sensitive actions correctly implement the twoFactorRequired: true protection. An attacker who has hijacked a session can enable 2FA on the victim's account, which also logs out all other sessions of the victim, potentially locking them out and maintaining the attacker's access.

Steps to Reproduce
  1. Authenticate as a user and obtain the X-Auth-Token and X-User-Id.
  2. Send a POST request to /api/v1/users.2fa.enableEmail with the authentication headers.
  3. Observe that the request succeeds and 2FA is enabled without any password prompt or 2FA challenge, and all other active sessions for the user are logged out.
Trace
graph TD
    subgraph SG0 ["./Rocket.Chat/apps/meteor/app/2fa/server/code/index.ts"]
        getUserForCheck["getUserForCheck"]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["./Rocket.Chat/apps/meteor/app/2fa/server/functions/resetTOTP.ts"]
        sendResetNotification["sendResetNotification"]
        resetTOTP["Resets the TOTP configuration for a user, including notifying the user via email."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["./Rocket.Chat/apps/meteor/app/api/server/ApiClass.ts"]
        APIClass.success["APIClass.success"]
        APIClass.failure["APIClass.failure"]
        APIClass.forbidden["APIClass.forbidden"]
        APIClass.this.parseJsonQuery["APIClass.this.parseJsonQuery"]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["./Rocket.Chat/apps/meteor/app/api/server/helpers/getPaginationItems.ts"]
        getPaginationItems["Calculates and validates pagination offset and count parameters based on API settings."]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["./Rocket.Chat/apps/meteor/app/api/server/helpers/getUserFromParams.ts"]
        getUserFromParams["Retrieves user record from provided request parameters (userId/username)."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["./Rocket.Chat/apps/meteor/app/api/server/helpers/getUserInfo.ts"]
        isVerifiedEmail["isVerifiedEmail"]
        getUserPreferences["getUserPreferences"]
        filterOutdatedVersionUpdateBanners["filterOutdatedVersionUpdateBanners"]
        getUserCalendar["getUserCalendar"]
        getUserInfo["Retrieves and filters user information, including preferences, calendar settings, and service status for API responses."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["./Rocket.Chat/apps/meteor/app/api/server/helpers/isUserFromParams.ts"]
        isUserFromParams["Determines if the user specified in params matches the logged-in user."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["./Rocket.Chat/apps/meteor/app/api/server/lib/eraseTeam.ts"]
        eraseTeamOnRelinquishRoomOwnerships["eraseTeamOnRelinquishRoomOwnerships"]
        eraseRoomLooseValidation["eraseRoomLooseValidation"]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["./Rocket.Chat/apps/meteor/app/api/server/lib/getUploadFormData.ts"]
        getUploadFormData["Extracts and validates form data from a multipart upload request."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["./Rocket.Chat/apps/meteor/app/api/server/lib/isValidQuery.ts"]
        ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts["Validates query objects against allowed attributes and operations."]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG10 ["./Rocket.Chat/apps/meteor/app/api/server/lib/users.ts"]
        findUsersToAutocomplete["Finds users for autocomplete based on search term and conditions."]
        getInclusiveFields["Extracts inclusive fields from a query object."]
        getNonEmptyFields["Returns default fields if input fields are empty."]
        getNonEmptyQuery["Returns a default query if input query is empty."]
        findPaginatedUsersByStatus["Finds paginated users based on status and other criteria."]
    end
    style SG10 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG11 ["./Rocket.Chat/apps/meteor/app/api/server/v1/users.ts"]
        get["get"]
        action{{"action"}}
    end
    style SG11 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG12 ["./Rocket.Chat/apps/meteor/app/authentication/server/startup/index.js"]
        Accounts.insertUserDoc["Accounts.insertUserDoc"]
    end
    style SG12 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG13 ["./Rocket.Chat/apps/meteor/app/authorization/server/functions/hasPermission.ts"]
        hasPermissionAsync["Checks if a user has a specific permission within a given scope."]
    end
    style SG13 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG14 ["./Rocket.Chat/apps/meteor/app/authorization/server/index.ts"]
        ._Rocket.Chat_apps_meteor_app_authorization_server_index.ts["Exports authorization-related functions and registers server-side methods for permission management."]
    end
    style SG14 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG15 ["./Rocket.Chat/apps/meteor/app/file/server/file.server.ts"]
        RocketChatFile.dataURIParse["RocketChatFile.dataURIParse"]
    end
    style SG15 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG16 ["./Rocket.Chat/apps/meteor/app/invites/server/functions/validateInviteToken.ts"]
        validateInviteToken["validateInviteToken"]
    end
    style SG16 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG17 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts"]
        addUserToDefaultChannels["addUserToDefaultChannels"]
    end
    style SG17 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG18 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/addUserToRoom.ts"]
        addUserToRoom["addUserToRoom"]
    end
    style SG18 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG19 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/checkEmailAvailability.ts"]
        checkEmailAvailability["Checks if an email address is available in the system."]
    end
    style SG19 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG20 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts"]
        toRegExp["toRegExp"]
        usernameIsBlocked["usernameIsBlocked"]
        checkUsernameAvailabilityWithValidation["Checks username availability with validation checks against user state."]
        checkUsernameAvailability["Checks if a username is available or blocked."]
    end
    style SG20 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG21 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/deleteRoom.ts"]
        deleteRoom["deleteRoom"]
    end
    style SG21 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG22 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/deleteUser.ts"]
        deleteUser["Deletes a user account, managing room ownership, message erasure, and associated data cleanup."]
    end
    style SG22 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG23 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.ts"]
        getAvatarSuggestionForUser["Suggests and fetches avatar images for a user from various providers."]
    end
    style SG23 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG24 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/getDefaultChannels.ts"]
        getDefaultChannels["getDefaultChannels"]
    end
    style SG24 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG25 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/getFullUserData.ts"]
        getCustomFields["getCustomFields"]
        getFields["getFields"]
        findTargetUser["findTargetUser"]
        getFullUserDataByUniqueSearchTerm["Retrieves full user data based on a unique search term, enforcing permission checks."]
    end
    style SG25 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG26 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts"]
        shouldRemoveOrChangeOwner["Checks if any rooms require ownership changes or removal."]
        getSubscribedRoomsForUserWithDetails["Retrieves rooms a user is subscribed to and determines ownership status for each."]
    end
    style SG26 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG27 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/getUserSingleOwnedRooms.ts"]
        getUserSingleOwnedRooms["Identifies rooms where the user is the sole owner."]
    end
    style SG27 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG28 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts"]
        slug["slug"]
        usernameIsAvailable["usernameIsAvailable"]
        name["name"]
        generateUsernameSuggestion["Generates a unique username suggestion based on user profile information."]
    end
    style SG28 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG29 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/joinDefaultChannels.ts"]
        joinDefaultChannels["joinDefaultChannels"]
    end
    style SG29 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG30 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts"]
        bulkTeamCleanup["bulkTeamCleanup"]
        bulkRoomCleanUp["Performs bulk cleanup of rooms, including removing files, subscriptions, and messages."]
        relinquishRoomOwnerships["Relinquishes room ownerships for a user, potentially reassigning ownership or removing rooms."]
    end
    style SG30 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG31 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveCustomFields.ts"]
        saveCustomFields["Validates and saves custom user fields."]
    end
    style SG31 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG32 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts"]
        getCustomFieldsMeta["getCustomFieldsMeta"]
        saveCustomFieldsWithoutValidation["Saves custom user fields to the database without performing schema validation."]
    end
    style SG32 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG33 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveUser/index.ts"]
        ._Rocket.Chat_apps_meteor_app_lib_server_functions_saveUser_index.ts["Exports functions for user saving and validation."]
    end
    style SG33 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG34 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts"]
        sendUserEmail["sendUserEmail"]
        sendWelcomeEmail["Sends a welcome email to a newly created user."]
    end
    style SG34 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG35 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts"]
        canEditExtension["Checks if a VoIP extension is available for assignment to a user."]
    end
    style SG35 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG36 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/saveUserIdentity.ts"]
        saveUserIdentity["saveUserIdentity"]
        updateUsernameReferences["updateUsernameReferences"]
    end
    style SG36 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG37 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/setRealName.ts"]
        setRealName["setRealName"]
    end
    style SG37 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG38 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/setStatusText.ts"]
        setStatusText["Updates a user's status text and broadcasts the change to other users."]
    end
    style SG38 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG39 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/setUserAvatar.ts"]
        setAvatarFromServiceWithValidation["setAvatarFromServiceWithValidation"]
        setUserAvatar["Sets a user's avatar from a data URI, URL, or raw buffer, performing validation and database updates."]
    end
    style SG39 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG40 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/setUsername.ts"]
        isUserInFederatedRooms["isUserInFederatedRooms"]
        setUsernameWithValidation["Sets a user's username after performing various validations and checks."]
        setUsername["_setUsername"]
    end
    style SG40 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG41 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts"]
        getFname["getFname"]
        getName["getName"]
        getUsersWhoAreInTheSameGroupDMsAs["getUsersWhoAreInTheSameGroupDMsAs"]
        updateGroupDMsName["updateGroupDMsName"]
        getMembers["getMembers"]
    end
    style SG41 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG42 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/validateCustomFields.js"]
        validateCustomFields["Validates user-provided custom fields against configured metadata."]
    end
    style SG42 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG43 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/validateName.ts"]
        validateName["validateName"]
    end
    style SG43 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG44 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/validateNameChars.ts"]
        validateNameChars["Validates that a name string does not contain invalid characters."]
    end
    style SG44 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG45 ["./Rocket.Chat/apps/meteor/app/lib/server/functions/validateUsername.ts"]
        validateUsername["Validates a username against configured regex patterns."]
    end
    style SG45 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG46 ["./Rocket.Chat/apps/meteor/app/lib/server/index.ts"]
        ._Rocket.Chat_apps_meteor_app_lib_server_index.ts["./Rocket.Chat/apps/meteor/app/lib/server/index.ts"]
    end
    style SG46 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG47 ["./Rocket.Chat/apps/meteor/app/lib/server/lib/notifyListener.ts"]
        notifyOnRoomChangedById["Broadcasts a notification that a room has changed, identified by ID."]
        notifyOnRoomChangedByUsernamesOrUids["notifyOnRoomChangedByUsernamesOrUids"]
        notifyOnIntegrationChangedByUserId["notifyOnIntegrationChangedByUserId"]
        notifyOnLivechatDepartmentAgentChanged["notifyOnLivechatDepartmentAgentChanged"]
        notifyOnSettingChanged["Broadcasts a notification that a system setting has changed."]
        notifyOnSettingChangedById["notifyOnSettingChangedById"]
        notifyOnUserChange["Broadcasts a notification about a user change event."]
        notifyOnUserChangeAsync["Executes a callback and broadcasts user change notifications based on the result."]
        notifyOnSubscriptionChanged["notifyOnSubscriptionChanged"]
        notifyOnSubscriptionChangedByRoomIdAndUserId["notifyOnSubscriptionChangedByRoomIdAndUserId"]
        notifyOnSubscriptionChangedById["notifyOnSubscriptionChangedById"]
        notifyOnSubscriptionChangedByUserPreferences["notifyOnSubscriptionChangedByUserPreferences"]
        notifyOnSubscriptionChangedByRoomId["notifyOnSubscriptionChangedByRoomId"]
        notifyOnSubscriptionChangedByAutoTranslateAndUserId["notifyOnSubscriptionChangedByAutoTranslateAndUserId"]
        notifyOnSubscriptionChangedByUserIdAndRoomType["notifyOnSubscriptionChangedByUserIdAndRoomType"]
        notifyOnSubscriptionChangedByNameAndRoomType["Broadcasts a notification that subscriptions matching a name and room type have changed."]
        notifyOnSubscriptionChangedByUserId["notifyOnSubscriptionChangedByUserId"]
    end
    style SG47 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG48 ["./Rocket.Chat/apps/meteor/app/lib/server/methods/createToken.ts"]
        generateAccessToken["Generates an access token for a user if the provided secret is valid."]
    end
    style SG48 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG49 ["./Rocket.Chat/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts"]
        deleteUserOwnAccount["Deletes the current user's account after verifying their password."]
    end
    style SG49 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG50 ["./Rocket.Chat/apps/meteor/app/mailer/server/api.ts"]
        replacekey["replacekey"]
        translate["translate"]
        replace["Replaces placeholders in a string with values from the provided data object."]
        replaceEscaped["replaceEscaped"]
        wrap["wrap"]
        checkAddressFormat["Validates the format of one or more email addresses."]
        sendNoWrap["Sends an email without applying standard layout wrapping, triggering pre-send events."]
        send["send"]
    end
    style SG50 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG51 ["./Rocket.Chat/apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts"]
        getDefaultSubscriptionPref["getDefaultSubscriptionPref"]
    end
    style SG51 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG52 ["./Rocket.Chat/apps/meteor/app/utils/lib/getURL.ts"]
        getCloudUrl["getCloudUrl"]
        getURL_2["_getURL"]
        getURLWithoutSettings["getURLWithoutSettings"]
    end
    style SG52 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG53 ["./Rocket.Chat/apps/meteor/app/utils/lib/mimeTypes.ts"]
        getMimeTypeFromFileName["getMimeTypeFromFileName"]
        getMimeType["getMimeType"]
    end
    style SG53 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG54 ["./Rocket.Chat/apps/meteor/app/utils/server/functions/isSMTPConfigured.ts"]
        isSMTPConfigured["Checks if SMTP configuration is set in environment or settings."]
    end
    style SG54 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG55 ["./Rocket.Chat/apps/meteor/app/utils/server/getURL.ts"]
        getURL["Constructs a URL based on the provided path and configuration settings."]
    end
    style SG55 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG56 ["./Rocket.Chat/apps/meteor/app/utils/server/lib/getUserPreference.ts"]
        getUserPreference["getUserPreference"]
    end
    style SG56 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG57 ["./Rocket.Chat/apps/meteor/client/meteor/overrides/userAndUsers.ts"]
        Meteor.userId["Meteor.userId"]
    end
    style SG57 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG58 ["./Rocket.Chat/apps/meteor/client/meteor/user.ts"]
        watchUserId["watchUserId"]
    end
    style SG58 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG59 ["./Rocket.Chat/apps/meteor/client/meteor/watch.ts"]
        watch["watch"]
    end
    style SG59 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG60 ["./Rocket.Chat/apps/meteor/imports/personal-access-tokens/server/api/methods/generateToken.ts"]
        generatePersonalAccessTokenOfUser["Generates a new personal access token for a user and stores it in the database."]
    end
    style SG60 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG61 ["./Rocket.Chat/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts"]
        regeneratePersonalAccessTokenOfUser["Regenerates a user's personal access token by removing the old one and creating a new one."]
    end
    style SG61 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG62 ["./Rocket.Chat/apps/meteor/imports/personal-access-tokens/server/api/methods/removeToken.ts"]
        removePersonalAccessTokenOfUser["Removes a user's personal access token from the database."]
    end
    style SG62 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG63 ["./Rocket.Chat/apps/meteor/lib/roles/calculateRoomRolePriorityFromRoles.ts"]
        calculateRoomRolePriorityFromRoles["calculateRoomRolePriorityFromRoles"]
    end
    style SG63 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG64 ["./Rocket.Chat/apps/meteor/lib/utils/isObject.ts"]
        isObject["isObject"]
    end
    style SG64 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG65 ["./Rocket.Chat/apps/meteor/lib/utils/parseCSV.ts"]
        parseCSV["Parses a comma-separated string into an array of trimmed items, optionally removing empty values."]
    end
    style SG65 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG66 ["./Rocket.Chat/apps/meteor/lib/utils/stringUtils.ts"]
        makeString["makeString"]
        defaultToWhiteSpace["defaultToWhiteSpace"]
        trim["Trims whitespace or specified characters from both ends of a string."]
        ltrim["Trims whitespace or specified characters from the left side of a string."]
        rtrim["Trims whitespace or specified characters from the right side of a string."]
        strLeft["strLeft"]
        strRightBack["strRightBack"]
    end
    style SG66 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG67 ["./Rocket.Chat/apps/meteor/server/database/utils.ts"]
        isExtendedSession["isExtendedSession"]
        onceTransactionCommitedSuccessfully["onceTransactionCommitedSuccessfully"]
        withError["withError"]
    end
    style SG67 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG68 ["./Rocket.Chat/apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts"]
        getSubscriptionAutotranslateDefaultConfig["getSubscriptionAutotranslateDefaultConfig"]
    end
    style SG68 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG69 ["./Rocket.Chat/apps/meteor/server/lib/isUserIdFederated.ts"]
        isUserIdFederated["isUserIdFederated"]
    end
    style SG69 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG70 ["./Rocket.Chat/apps/meteor/server/lib/resetUserE2EKey.ts"]
        sendResetNotification_2["sendResetNotification"]
        resetUserE2EEncriptionKey["Resets a user's E2E encryption key, notifies the user, and forces a logout."]
    end
    style SG70 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG71 ["./Rocket.Chat/apps/meteor/server/lib/roles/addUserRoles.ts"]
        addUserRolesAsync["Adds roles to a user, optionally scoped to a specific room, with validation."]
    end
    style SG71 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG72 ["./Rocket.Chat/apps/meteor/server/lib/roles/syncRoomRolePriority.ts"]
        syncRoomRolePriorityForUserAndRoom["syncRoomRolePriorityForUserAndRoom"]
        updateRolePriority["updateRolePriority"]
    end
    style SG72 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG73 ["./Rocket.Chat/apps/meteor/server/lib/roles/validateRoleList.ts"]
        validateRoleList["Validates a list of role IDs against the database."]
    end
    style SG73 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG74 ["./Rocket.Chat/apps/meteor/server/lib/rooms/roomCoordinator.ts"]
        RoomCoordinatorServer.allowMemberAction["RoomCoordinatorServer.allowMemberAction"]
        RoomCoordinatorServer.getRoomDirectives["RoomCoordinatorServer.getRoomDirectives"]
    end
    style SG74 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG75 ["./Rocket.Chat/apps/meteor/server/methods/registerUser.ts"]
        registerUser["Registers a new user account with provided credentials and optional metadata."]
    end
    style SG75 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG76 ["./Rocket.Chat/apps/meteor/server/methods/requestDataDownload.ts"]
        requestDataDownload["Initiates a data export operation for a user."]
    end
    style SG76 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG77 ["./Rocket.Chat/apps/meteor/server/methods/resetAvatar.ts"]
        resetAvatar["Resets a user's avatar, requiring appropriate permissions."]
        userId["userId"]
    end
    style SG77 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG78 ["./Rocket.Chat/apps/meteor/server/methods/saveUserPreferences.ts"]
        updateNotificationPreferences["updateNotificationPreferences"]
        saveUserPreferences["Updates user preferences and propagates changes to notification settings."]
    end
    style SG78 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG79 ["./Rocket.Chat/apps/meteor/server/methods/sendConfirmationEmail.ts"]
        sendConfirmationEmail["Sends a verification email to a user identified by their email address."]
    end
    style SG79 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG80 ["./Rocket.Chat/apps/meteor/server/methods/sendForgotPasswordEmail.ts"]
        sendForgotPasswordEmail["Sends a password reset email to a user identified by their email address."]
    end
    style SG80 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG81 ["./Rocket.Chat/apps/meteor/server/methods/setUserActiveStatus.ts"]
        executeSetUserActiveStatus["Executes the logic to change a user's active status, verifying permissions."]
        setUserActiveStatus["setUserActiveStatus"]
    end
    style SG81 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG82 ["./Rocket.Chat/apps/meteor/server/services/authorization/service.ts"]
        Authorization.hasPermission["Authorization.hasPermission"]
        Authorization.all["Authorization.all"]
    end
    style SG82 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG83 ["./Rocket.Chat/apps/meteor/server/services/user/lib/getNewUserRoles.ts"]
        getNewUserRoles["Determines the default roles for a new user based on settings."]
    end
    style SG83 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG84 ["./Rocket.Chat/ee/apps/account-service/src/lib/utils.ts"]
        generateStampedLoginToken["Generates a stamped login token."]
        hashLoginToken["Hashes a login token for storage."]
    end
    style SG84 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG85 ["./Rocket.Chat/packages/models/src/models/BaseRaw.ts"]
        BaseRaw.doNotMixInclusionAndExclusionFields["BaseRaw.doNotMixInclusionAndExclusionFields"]
        BaseRaw.ensureDefaultFields["BaseRaw.ensureDefaultFields"]
        BaseRaw.find["BaseRaw.find"]
        BaseRaw.updateOne["Updates a record in the collection and ensures the 'updatedAt' field is set."]
        BaseRaw.updateMany["BaseRaw.updateMany"]
        BaseRaw.deleteMany["BaseRaw.deleteMany"]
        BaseRaw.setUpdatedAt["Internal helper to update the 'updatedAt' field on a record before persistence."]
    end
    style SG85 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG86 ["./Rocket.Chat/packages/models/src/models/LivechatDepartmentAgents.ts"]
        LivechatDepartmentAgentsRaw.findByAgentId["LivechatDepartmentAgentsRaw.findByAgentId"]
        LivechatDepartmentAgentsRaw.removeByAgentId["LivechatDepartmentAgentsRaw.removeByAgentId"]
        LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId["LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId"]
    end
    style SG86 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG87 ["./Rocket.Chat/packages/sha256/src/binb2hex.ts"]
        binb2hex["Converts a bit-packed number array to a hexadecimal string."]
    end
    style SG87 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG88 ["./Rocket.Chat/packages/sha256/src/core.ts"]
        safeAdd["safeAdd"]
        s["s"]
        r["r"]
        ch["ch"]
        maj["maj"]
        sigma0256["sigma0256"]
        sigma1256["sigma1256"]
        gamma0256["gamma0256"]
        gamma1256["gamma1256"]
        core["Core SHA-256 algorithm implementation."]
    end
    style SG88 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG89 ["./Rocket.Chat/packages/sha256/src/sha256.ts"]
        SHA256["Main entry point for SHA-256 hashing."]
    end
    style SG89 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG90 ["./Rocket.Chat/packages/sha256/src/str2binb.ts"]
        str2binb["Converts a string to a bit-packed number array for hashing."]
    end
    style SG90 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG91 ["./Rocket.Chat/packages/sha256/src/utf8Encode.ts"]
        utf8Encode["Encodes a string into UTF-8 format."]
    end
    style SG91 fill:#2a2a2a,stroke:#444,color:#aaa
    action --> isSMTPConfigured
    action --> resetTOTP
    action --> getPaginationItems
    action --> getUserFromParams
    action --> isUserFromParams
    action --> getUserInfo
    action --> registerUser
    action --> requestDataDownload
    action --> executeSetUserActiveStatus
    action --> saveUserPreferences
    action --> sendForgotPasswordEmail
    action --> sendConfirmationEmail
    action --> resetAvatar
    action --> getUploadFormData
    action --> findUsersToAutocomplete
    action --> findPaginatedUsersByStatus
    action --> ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts
    action --> hasPermissionAsync
    action --> resetUserE2EEncriptionKey
    action --> validateNameChars
    action --> sendWelcomeEmail
    action --> ._Rocket.Chat_apps_meteor_app_lib_server_functions_saveUser_index.ts
    action --> canEditExtension
    action --> setUsernameWithValidation
    action --> saveCustomFields
    action --> saveCustomFieldsWithoutValidation
    action --> deleteUser
    action --> generateUsernameSuggestion
    action --> getAvatarSuggestionForUser
    action --> generatePersonalAccessTokenOfUser
    action --> regeneratePersonalAccessTokenOfUser
    action --> removePersonalAccessTokenOfUser
    action --> getFullUserDataByUniqueSearchTerm
    action --> checkEmailAvailability
    action --> checkUsernameAvailabilityWithValidation
    action --> checkUsernameAvailability
    action --> validateUsername
    action --> setStatusText
    action --> validateCustomFields
    action --> setUserAvatar
    action --> deleteUserOwnAccount
    action --> notifyOnUserChange
    action --> notifyOnUserChangeAsync
    action --> hashLoginToken
    action --> getUserForCheck
    action --> APIClass.success
    action --> APIClass.failure
    action --> APIClass.forbidden
    action --> APIClass.this.parseJsonQuery
    action --> get
    action --> generateAccessToken
    resetTOTP --> sendResetNotification
    resetTOTP --> isUserIdFederated
    resetTOTP --> notifyOnUserChange
    getUserInfo --> getURL
    getUserInfo --> isVerifiedEmail
    getUserInfo --> getUserPreferences
    getUserInfo --> filterOutdatedVersionUpdateBanners
    getUserInfo --> getUserCalendar
    registerUser --> validateInviteToken
    registerUser --> Accounts.insertUserDoc
    registerUser --> registerUser
    registerUser --> ._Rocket.Chat_apps_meteor_app_lib_server_index.ts
    registerUser --> trim
    registerUser --> generateStampedLoginToken
    requestDataDownload --> requestDataDownload
    executeSetUserActiveStatus --> setUserActiveStatus
    executeSetUserActiveStatus --> hasPermissionAsync
    saveUserPreferences --> updateNotificationPreferences
    saveUserPreferences --> saveUserPreferences
    saveUserPreferences --> Meteor.userId
    saveUserPreferences --> notifyOnUserChange
    saveUserPreferences --> notifyOnSubscriptionChangedByAutoTranslateAndUserId
    saveUserPreferences --> notifyOnSubscriptionChangedByUserId
    sendForgotPasswordEmail --> sendForgotPasswordEmail
    resetAvatar --> resetAvatar
    resetAvatar --> userId
    resetAvatar --> hasPermissionAsync
    getUploadFormData --> getMimeType
    findUsersToAutocomplete --> hasPermissionAsync
    findPaginatedUsersByStatus --> hasPermissionAsync
    hasPermissionAsync --> Authorization.hasPermission
    resetUserE2EEncriptionKey --> isUserIdFederated
    resetUserE2EEncriptionKey --> sendResetNotification_2
    resetUserE2EEncriptionKey --> notifyOnUserChange
    resetUserE2EEncriptionKey --> notifyOnSubscriptionChangedByUserId
    sendWelcomeEmail --> sendUserEmail
    setUsernameWithValidation --> saveUserIdentity
    setUsernameWithValidation --> isUserInFederatedRooms
    setUsernameWithValidation --> checkUsernameAvailability
    setUsernameWithValidation --> joinDefaultChannels
    setUsernameWithValidation --> validateUsername
    setUsernameWithValidation --> notifyOnUserChange
    saveCustomFields --> saveCustomFieldsWithoutValidation
    saveCustomFields --> validateCustomFields
    saveCustomFields --> trim
    saveCustomFieldsWithoutValidation --> getCustomFieldsMeta
    saveCustomFieldsWithoutValidation --> onceTransactionCommitedSuccessfully
    saveCustomFieldsWithoutValidation --> notifyOnSubscriptionChangedByUserIdAndRoomType
    saveCustomFieldsWithoutValidation --> trim
    deleteUser --> LivechatDepartmentAgentsRaw.findByAgentId
    deleteUser --> LivechatDepartmentAgentsRaw.removeByAgentId
    deleteUser --> getUserSingleOwnedRooms
    deleteUser --> updateGroupDMsName
    deleteUser --> shouldRemoveOrChangeOwner
    deleteUser --> getSubscribedRoomsForUserWithDetails
    deleteUser --> relinquishRoomOwnerships
    deleteUser --> notifyOnRoomChangedById
    deleteUser --> notifyOnIntegrationChangedByUserId
    deleteUser --> notifyOnLivechatDepartmentAgentChanged
    deleteUser --> notifyOnUserChange
    generateUsernameSuggestion --> slug
    generateUsernameSuggestion --> usernameIsAvailable
    generateUsernameSuggestion --> name
    generatePersonalAccessTokenOfUser --> hasPermissionAsync
    generatePersonalAccessTokenOfUser --> hashLoginToken
    regeneratePersonalAccessTokenOfUser --> hasPermissionAsync
    regeneratePersonalAccessTokenOfUser --> generatePersonalAccessTokenOfUser
    regeneratePersonalAccessTokenOfUser --> removePersonalAccessTokenOfUser
    removePersonalAccessTokenOfUser --> hasPermissionAsync
    removePersonalAccessTokenOfUser --> removePersonalAccessTokenOfUser
    getFullUserDataByUniqueSearchTerm --> hasPermissionAsync
    getFullUserDataByUniqueSearchTerm --> getFields
    getFullUserDataByUniqueSearchTerm --> findTargetUser
    checkUsernameAvailabilityWithValidation --> checkUsernameAvailability
    checkUsernameAvailability --> toRegExp
    checkUsernameAvailability --> usernameIsBlocked
    checkUsernameAvailability --> validateName
    setStatusText --> onceTransactionCommitedSuccessfully
    validateCustomFields --> trim
    setUserAvatar --> RocketChatFile.dataURIParse
    setUserAvatar --> onceTransactionCommitedSuccessfully
    deleteUserOwnAccount --> SHA256
    deleteUserOwnAccount --> deleteUser
    deleteUserOwnAccount --> deleteUserOwnAccount
    deleteUserOwnAccount --> Meteor.userId
    deleteUserOwnAccount --> trim
    notifyOnUserChangeAsync --> notifyOnUserChange
    APIClass.success --> isObject
    APIClass.failure --> isObject
    APIClass.this.parseJsonQuery --> APIClass.this.parseJsonQuery
    get --> getURL
    get --> getPaginationItems
    get --> getUserFromParams
    get --> getInclusiveFields
    get --> getNonEmptyFields
    get --> getNonEmptyQuery
    get --> ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts
    get --> hasPermissionAsync
    get --> APIClass.success
    get --> APIClass.forbidden
    get --> APIClass.this.parseJsonQuery
    get --> get
    generateAccessToken --> generateStampedLoginToken
    sendResetNotification --> send
    getURL --> getURLWithoutSettings
    getUserPreferences --> getUserPreference
    Accounts.insertUserDoc --> getNewUserRoles
    Accounts.insertUserDoc --> addUserRolesAsync
    Accounts.insertUserDoc --> getAvatarSuggestionForUser
    Accounts.insertUserDoc --> joinDefaultChannels
    Accounts.insertUserDoc --> setAvatarFromServiceWithValidation
    Accounts.insertUserDoc --> notifyOnSettingChangedById
    Accounts.insertUserDoc --> parseCSV
    trim --> makeString
    trim --> defaultToWhiteSpace
    setUserActiveStatus --> executeSetUserActiveStatus
    setUserActiveStatus --> Meteor.userId
    updateNotificationPreferences --> notifyOnSubscriptionChangedByUserPreferences
    Meteor.userId --> watchUserId
    getMimeType --> getMimeTypeFromFileName
    Authorization.hasPermission --> Authorization.all
    sendResetNotification_2 --> send
    sendUserEmail --> send
    saveUserIdentity --> updateUsernameReferences
    saveUserIdentity --> setUsername
    saveUserIdentity --> onceTransactionCommitedSuccessfully
    saveUserIdentity --> setRealName
    saveUserIdentity --> validateName
    joinDefaultChannels --> addUserToDefaultChannels
    onceTransactionCommitedSuccessfully --> isExtendedSession
    onceTransactionCommitedSuccessfully --> withError
    LivechatDepartmentAgentsRaw.findByAgentId --> BaseRaw.find
    LivechatDepartmentAgentsRaw.removeByAgentId --> BaseRaw.deleteMany
    updateGroupDMsName --> getFname
    updateGroupDMsName --> getName
    updateGroupDMsName --> getUsersWhoAreInTheSameGroupDMsAs
    updateGroupDMsName --> getMembers
    updateGroupDMsName --> notifyOnSubscriptionChangedByRoomId
    getSubscribedRoomsForUserWithDetails --> ._Rocket.Chat_apps_meteor_app_authorization_server_index.ts
    relinquishRoomOwnerships --> addUserRolesAsync
    relinquishRoomOwnerships --> bulkRoomCleanUp
    name --> slug
    getFields --> getCustomFields
    SHA256 --> utf8Encode
    SHA256 --> str2binb
    SHA256 --> binb2hex
    SHA256 --> core
    send --> replace
    send --> wrap
    send --> sendNoWrap
    getURLWithoutSettings --> getURL_2
    getNewUserRoles --> parseCSV
    addUserRolesAsync --> validateRoleList
    addUserRolesAsync --> syncRoomRolePriorityForUserAndRoom
    addUserRolesAsync --> notifyOnSubscriptionChangedByRoomIdAndUserId
    setAvatarFromServiceWithValidation --> hasPermissionAsync
    setAvatarFromServiceWithValidation --> setUserAvatar
    defaultToWhiteSpace --> makeString
    watchUserId --> watch
    updateUsernameReferences --> LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId
    updateUsernameReferences --> updateGroupDMsName
    updateUsernameReferences --> notifyOnRoomChangedByUsernamesOrUids
    updateUsernameReferences --> notifyOnSubscriptionChangedByNameAndRoomType
    updateUsernameReferences --> notifyOnSubscriptionChangedByUserId
    setUsername --> isUserInFederatedRooms
    setUsername --> onceTransactionCommitedSuccessfully
    setUsername --> getAvatarSuggestionForUser
    setUsername --> checkUsernameAvailability
    setUsername --> validateUsername
    setUsername --> addUserToRoom
    setUsername --> setUserAvatar
    setRealName --> onceTransactionCommitedSuccessfully
    addUserToDefaultChannels --> getDefaultSubscriptionPref
    addUserToDefaultChannels --> getSubscriptionAutotranslateDefaultConfig
    addUserToDefaultChannels --> getDefaultChannels
    addUserToDefaultChannels --> notifyOnSubscriptionChangedById
    BaseRaw.find --> BaseRaw.doNotMixInclusionAndExclusionFields
    BaseRaw.find --> BaseRaw.find
    BaseRaw.deleteMany --> BaseRaw.find
    BaseRaw.deleteMany --> BaseRaw.updateOne
    BaseRaw.deleteMany --> BaseRaw.deleteMany
    bulkRoomCleanUp --> eraseRoomLooseValidation
    bulkRoomCleanUp --> bulkTeamCleanup
    bulkRoomCleanUp --> notifyOnSubscriptionChanged
    core --> safeAdd
    core --> ch
    core --> maj
    core --> sigma0256
    core --> sigma1256
    core --> gamma0256
    core --> gamma1256
    replace --> replacekey
    replace --> translate
    replace --> replace
    replace --> strLeft
    replace --> strRightBack
    wrap --> replace
    wrap --> replaceEscaped
    sendNoWrap --> checkAddressFormat
    sendNoWrap --> notifyOnSettingChanged
    getURL_2 --> getCloudUrl
    getURL_2 --> trim
    getURL_2 --> ltrim
    getURL_2 --> rtrim
    syncRoomRolePriorityForUserAndRoom --> updateRolePriority
    syncRoomRolePriorityForUserAndRoom --> calculateRoomRolePriorityFromRoles
    LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId --> BaseRaw.updateMany
    addUserToRoom --> RoomCoordinatorServer.allowMemberAction
    addUserToRoom --> RoomCoordinatorServer.getRoomDirectives
    addUserToRoom --> notifyOnRoomChangedById
    addUserToRoom --> notifyOnSubscriptionChanged
    BaseRaw.doNotMixInclusionAndExclusionFields --> BaseRaw.ensureDefaultFields
    BaseRaw.updateOne --> BaseRaw.updateOne
    BaseRaw.updateOne --> BaseRaw.setUpdatedAt
    eraseRoomLooseValidation --> deleteRoom
    bulkTeamCleanup --> eraseTeamOnRelinquishRoomOwnerships
    sigma0256 --> s
    sigma1256 --> s
    gamma0256 --> s
    gamma0256 --> r
    gamma1256 --> s
    gamma1256 --> r
    replacekey --> replace
    strLeft --> makeString
    strRightBack --> makeString
    replaceEscaped --> replace
    getCloudUrl --> ltrim
    getCloudUrl --> rtrim
    ltrim --> makeString
    ltrim --> defaultToWhiteSpace
    rtrim --> makeString
    rtrim --> defaultToWhiteSpace
    updateRolePriority --> calculateRoomRolePriorityFromRoles
    BaseRaw.updateMany --> BaseRaw.updateMany
    BaseRaw.updateMany --> BaseRaw.setUpdatedAt
    BaseRaw.setUpdatedAt --> BaseRaw.setUpdatedAt
    deleteRoom --> notifyOnRoomChangedById
    deleteRoom --> notifyOnSubscriptionChanged
    eraseTeamOnRelinquishRoomOwnerships --> eraseRoomLooseValidation
Loading
Fix with AI

Open in Cursor Open in Claude

Fix the following security vulnerability found by Hacktron.

File: apps/meteor/app/api/server/v1/users.ts
Severity: medium

Vulnerability: Missing Re-authentication for 2FA Enablement

Description:
The `users.2fa.enableEmail` endpoint allows an authenticated user to enable email-based 2FA without requiring password re-authentication or a secondary 2FA challenge. In contrast, disabling email 2FA (`users.2fa.disableEmail`) and other sensitive actions correctly implement the `twoFactorRequired: true` protection. An attacker who has hijacked a session can enable 2FA on the victim's account, which also logs out all other sessions of the victim, potentially locking them out and maintaining the attacker's access.

Proof of Concept:
**Steps to Reproduce**

1. Authenticate as a user and obtain the `X-Auth-Token` and `X-User-Id`.
2. Send a POST request to `/api/v1/users.2fa.enableEmail` with the authentication headers.
3. Observe that the request succeeds and 2FA is enabled without any password prompt or 2FA challenge, and all other active sessions for the user are logged out.

Affected Code:
API.v1.post('users.2fa.enableEmail', { authRequired: true, response: { ... } }, async function action() { ... });

Fix this vulnerability. Only change what's necessary - don't modify unrelated code.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), or !accepted_risk <reason>. Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing. Any other reply is saved as a triage note.

View finding in Hacktron

@hacktron-app hacktron-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 issues found across 7 files

Severity Count
🔴 High 4
🟡 Medium 3
Comments Outside Diff (5)

🔴 High: Account Takeover via Unverified Email in Apple OAuth Login

Location: apps/meteor/app/apple/server/loginHandler.ts:32-35

The Apple OAuth login handler in loginHandler.ts contains a vulnerability where it trusts a client-provided email address if the Apple identity token does not contain one. Because the client-provided email field in the loginRequest is not cryptographically verified, an attacker can supply an arbitrary email address (e.g., a victim's email). The accounts_meld.js hook will then use this unverified email to look up an existing user and link the attacker's Apple service ID to the victim's account. This allows the attacker to log into the victim's account using their own Apple ID, leading to a full account takeover.

Steps to Reproduce

  1. The attacker initiates an Apple login using their own Apple ID, ensuring that the Apple identityToken does not contain an email claim (e.g., by using an Apple ID that hasn't shared its email, or during a subsequent login where Apple omits the email).
  2. The attacker intercepts the login request sent to the Rocket.Chat server (e.g., the DDP login method call).
  3. The attacker modifies the loginRequest payload to include the victim's email address in the email field: {"identityToken": "<attacker_token>", "email": "victim@example.com", "fullName": {...}}.
  4. The server processes the request. In loginHandler.ts, serviceData.email is empty, so it assigns serviceData.email = email (the victim's email).
  5. The accounts_meld.js hook looks up the user by the victim's email, finds the victim's account, and links the attacker's Apple ID to it (Users.setServiceId(user._id, serviceName, serviceData.id)).
  6. The attacker is now logged into the victim's account and can access it at any time using their Apple ID.

🔴 High: Insecure Account Linking via OAuth Email Claim

Location: apps/meteor/server/configuration/accounts_meld.js:21-24

The configureAccounts function in accounts_meld.js automatically links external OAuth accounts to existing local accounts based on the serviceData.email claim. It does not independently verify the email address or consistently check if the OAuth provider has verified the email (except for meteor-developer). If an attacker authenticates via an OAuth provider that allows unverified emails (or a custom OAuth provider), they can link their OAuth account to a victim's local account and achieve account takeover. Additionally, if the victim's local account is unverified, the attacker's login will trigger a password reset on the victim's account, locking them out.

Steps to Reproduce

  1. Identify a target user's email address (e.g., victim@example.com).
  2. Register an account on a supported OAuth provider (e.g., GitHub, GitLab, or a custom configured provider) using victim@example.com.
  3. Do not verify the email address on the OAuth provider if it allows unverified emails to be returned in the OAuth flow.
  4. Log into the Rocket.Chat instance using the OAuth provider.
  5. The configureAccounts logic will extract serviceData.email and link the OAuth account to the victim's local account.
  6. The attacker now has access to the victim's account.

🔴 High: Missing JWT Audience Validation in Apple OAuth Login

Location: apps/meteor/app/apple/lib/handleIdentityToken.ts:30-49

The handleIdentityToken function validates the signature of the Apple identity token but fails to verify the aud (audience) claim. This allows an attacker to use a valid Apple identity token issued for a different application to authenticate against the Rocket.Chat instance. If an attacker possesses a token signed by Apple, the system will accept it as valid for the Rocket.Chat instance, potentially leading to unauthorized account access.


🔴 High: Arbitrary Username Spoofing via Dolphin OAuth Provider

Location: apps/meteor/app/dolphin/server/lib.ts:23

The DolphinOnCreateUser function, registered as a beforeCreateUser callback, directly assigns user.username using the NickName field from the external Dolphin OAuth identity provider without any validation or collision checks. An attacker with control over an account on the external Dolphin OAuth provider can set their NickName to a reserved or existing system username (e.g., 'admin'), allowing them to impersonate other users or claim privileged identities during the account creation process in Rocket.Chat.

Steps to Reproduce

  1. Configure a Rocket.Chat instance with the Dolphin OAuth provider enabled.
  2. Create or gain access to an account on the external Dolphin OAuth identity provider.
  3. Modify the account's profile on the Dolphin provider to set the NickName field to a sensitive or reserved username (e.g., 'admin').
  4. Initiate the OAuth login flow on the Rocket.Chat instance using the Dolphin provider.
  5. Upon successful authentication, Rocket.Chat will create a new user account using the provided NickName as the system username.
  6. Verify that the new user has been created with the 'admin' username.

🟡 Medium: Login CSRF via URL Query Parameter (resumeToken)

Location: apps/meteor/client/views/root/hooks/useLoginViaQuery.ts:10

The useLoginViaQuery hook automatically processes a resumeToken from the URL query parameters and calls loginWithToken without any user interaction or secondary validation. An attacker can craft a malicious link containing a valid resumeToken (e.g., obtained through other means or previously leaked) and trick a victim into clicking it. This results in the victim's browser session being authenticated as the user associated with that token, leading to session fixation or account switching. While the hook attempts to clean the URL after the login, the authentication has already occurred.

Steps to Reproduce

  1. Obtain a valid resumeToken for a target user account (e.g., through log leakage, physical access, or other vulnerabilities).
  2. Construct a URL for the Rocket.Chat instance: https://example.rocket.chat/?resumeToken=VALID_TOKEN_HERE.
  3. Send this link to a victim user and trick them into clicking it.
  4. When the victim clicks the link, their browser will load the application, the useLoginViaQuery hook will execute, and the loginWithToken function will authenticate the victim's session using the provided token.
  5. The victim is now logged into the account associated with the token, potentially allowing the attacker to perform actions on behalf of the user or access their data.

View full scan results

gabriellsh and others added 3 commits May 25, 2026 18:30
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/meteor/client/lib/2fa/challenge2fa.ts (1)

24-25: ⚡ Quick win

Remove the inline implementation comments.

Please encode these constraints in the control flow or a follow-up issue instead of source comments. As per coding guidelines "Avoid code comments in the implementation".

Also applies to: 37-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/2fa/challenge2fa.ts` around lines 24 - 25, Remove the
inline implementation comments in apps/meteor/client/lib/2fa/challenge2fa.ts
(the two comment blocks about queuing and modal deterrence around the challenge
flow) and instead encode the constraints: implement or stub a simple queue/lock
around the 2FA challenge invocation (e.g., a single-running-ticket or
Promise-based lock used by the function that opens the modal) so concurrent
challenges cannot overwrite each other, or create a follow-up issue referenced
from the code (e.g., TODO with issue ID) if you cannot implement it now; ensure
no explanatory implementation comments remain in the function that manages
showing the modal and handling responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/meteor/client/lib/2fa/challenge2fa.spec.tsx`:
- Around line 189-202: The test "rejects the code promise when the dialog is
closed via the X button" currently clicks the same Cancel button; update the
test to click the dialog's dismiss (close-icon) control instead so the X-button
path is exercised: after calling challenge2fa(...) and asserting the dialog is
present, target the close icon via an accessible query such as
screen.getByRole('button', { name: /close/i }) or screen.getByLabelText('Close')
(whichever matches the modal implementation) and click that, then assert the
code promise rejects with 'Two-factor_authentication_cancelled'; keep the rest
of the test and references to challenge2fa unchanged.

In `@apps/meteor/client/lib/2fa/challenge2fa.ts`:
- Around line 143-150: The current branch overwrites the stored
unresolvedChallenge (used by saveChallenge) which can leave the first caller's
code promise unresolved; modify the logic in challenge2fa.ts around
unresolvedChallenge, saveChallenge, wrapWithEndChallenge, resolveChallenge and
rejectChallenge so concurrent challenges are not lost — either implement a
simple FIFO queue of pending challenge tuples and enqueue the new {onConfirm,
rejectChallenge, onClose, rejectCode} when unresolvedChallenge exists, or
explicitly reuse/cancel the existing tuple by rejecting the prior
unresolvedChallenge (calling its rejectChallenge with a distinct cancellation
error) before replacing it; ensure returned promises map to the correct queued
tuple (do not overwrite the previously stored callbacks without
resolving/rejecting them).

---

Nitpick comments:
In `@apps/meteor/client/lib/2fa/challenge2fa.ts`:
- Around line 24-25: Remove the inline implementation comments in
apps/meteor/client/lib/2fa/challenge2fa.ts (the two comment blocks about queuing
and modal deterrence around the challenge flow) and instead encode the
constraints: implement or stub a simple queue/lock around the 2FA challenge
invocation (e.g., a single-running-ticket or Promise-based lock used by the
function that opens the modal) so concurrent challenges cannot overwrite each
other, or create a follow-up issue referenced from the code (e.g., TODO with
issue ID) if you cannot implement it now; ensure no explanatory implementation
comments remain in the function that manages showing the modal and handling
responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fff747f5-ecbc-424f-b729-1c5335ac387d

📥 Commits

Reviewing files that changed from the base of the PR and between 068699b and 6be1d19.

📒 Files selected for processing (3)
  • apps/meteor/client/lib/2fa/challenge2fa.spec.tsx
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/errors.ts
📜 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). (5)
  • GitHub Check: 📦 Build Packages
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: CodeQL-Build
  • GitHub Check: Hacktron Security Check
  • GitHub Check: CodeQL-Build
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/playwright.mdc)

**/*.{ts,tsx,js}: Write concise, technical TypeScript/JavaScript with accurate typing in Playwright tests
Avoid code comments in the implementation

Files:

  • apps/meteor/client/lib/2fa/challenge2fa.spec.tsx
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/errors.ts
🧠 Learnings (7)
📚 Learning: 2026-03-06T18:10:15.268Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 39397
File: packages/gazzodown/src/code/CodeBlock.spec.tsx:47-68
Timestamp: 2026-03-06T18:10:15.268Z
Learning: In tests (especially those using testing-library/dom/jsdom) for Rocket.Chat components, the HTML <code> element has an implicit ARIA role of 'code'. Therefore, screen.getByRole('code') or screen.findByRole('code') will locate <code> elements even without a role attribute. Do not flag findByRole('code') as invalid in reviews; prefer using the implicit role instead of adding role="code" unless necessary for accessibility.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.spec.tsx
📚 Learning: 2026-03-27T14:52:56.865Z
Learnt from: dougfabris
Repo: RocketChat/Rocket.Chat PR: 39892
File: apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx:150-155
Timestamp: 2026-03-27T14:52:56.865Z
Learning: In Rocket.Chat, there are two different `ModalBackdrop` components with different prop APIs. During review, confirm the import source: (1) `rocket.chat/fuselage` `ModalBackdrop` uses `ModalBackdropProps` based on `BoxProps` (so it supports `onClick` and other Box/DOM props) and does not have an `onDismiss` prop; (2) `rocket.chat/ui-client` `ModalBackdrop` uses a narrower props interface like `{ children?: ReactNode; onDismiss?: () => void }` and handles Escape keypress and outside mouse-up, and it does not forward arbitrary DOM props such as `onClick`. Flag mismatched props (e.g., `onDismiss` passed to the fuselage component or `onClick` passed to the ui-client component) and ensure the usage matches the correct component being imported.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.spec.tsx
📚 Learning: 2026-05-06T12:21:44.083Z
Learnt from: juliajforesti
Repo: RocketChat/Rocket.Chat PR: 40256
File: apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx:121-149
Timestamp: 2026-05-06T12:21:44.083Z
Learning: Field wrappers in rocket.chat/fuselage-forms (Field, FieldLabel, FieldRow, FieldError, FieldHint) auto-create htmlFor/id associations, aria-describedby, and role="alert" for errors. Do not manually set htmlFor, id, aria-describedby, or role attributes when using these wrappers. This automatic wiring does not apply to plain rocket.chat/fuselage components, which require explicit ID wiring per the accessibility docs. In code reviews, prefer using fuselage-forms wrappers for form fields and verify there is no unnecessary manual ID/aria wiring in files that use these wrappers. If a component uses plain fuselage components, ensure proper id wiring as per docs.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.spec.tsx
  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/errors.ts
📚 Learning: 2026-02-10T16:32:42.586Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 38528
File: apps/meteor/client/startup/roles.ts:14-14
Timestamp: 2026-02-10T16:32:42.586Z
Learning: In Rocket.Chat's Meteor client code, DDP streams use EJSON and Date fields arrive as Date objects; do not manually construct new Date() in stream handlers (for example, in sdk.stream()). Only REST API responses return plain JSON where dates are strings, so implement explicit conversion there if needed. Apply this guidance to all TypeScript files under apps/meteor/client to ensure consistent date handling in DDP streams and REST responses.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.ts
📚 Learning: 2026-05-11T20:30:35.265Z
Learnt from: tassoevan
Repo: RocketChat/Rocket.Chat PR: 40480
File: apps/meteor/client/meteor/startup/accounts.ts:59-61
Timestamp: 2026-05-11T20:30:35.265Z
Learning: In Rocket.Chat’s Meteor client code, when calling `dispatchToastMessage` with `{ type: 'error' }`, pass the raw caught error object as `message` without manual normalization. `dispatchToastMessage` is designed to accept `message: unknown` for error toasts, so avoid converting errors to strings (e.g., `String(error)`) or extracting `error.message` before passing them.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In the Rocket.Chat repository, do not reference Biome lint rules in code review feedback. Biome is not used even if biome.json exists; only reference Biome rules if there is explicit, project-wide usage documented. For TypeScript files, review lint implications without Biome guidance unless the project enables Biome rules.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/errors.ts
📚 Learning: 2026-02-26T19:25:44.063Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 38778
File: packages/ui-voip/src/providers/useMediaSession.ts:192-192
Timestamp: 2026-02-26T19:25:44.063Z
Learning: In this repository (RocketChat/Rocket.Chat), Biome lint rules are not used even if a biome.json exists. When reviewing TypeScript files (e.g., packages/ui-voip/src/providers/useMediaSession.ts), ensure lint suggestions do not reference Biome-specific rules. Rely on general ESLint/TypeScript lint rules and project conventions instead.

Applied to files:

  • apps/meteor/client/lib/2fa/challenge2fa.ts
  • packages/api-client/src/errors.ts
🧬 Code graph analysis (1)
apps/meteor/client/lib/2fa/challenge2fa.spec.tsx (1)
apps/meteor/client/lib/2fa/challenge2fa.ts (1)
  • challenge2fa (96-168)
🔇 Additional comments (1)
packages/api-client/src/errors.ts (1)

21-23: Make sure every totp-* error payload includes a valid details.method (runtime behavior)

This change makes isTotp*Error return false unless hasRequiredTwoFactorMethod(error) passes, so any runtime path still forwarding only legacy { error } / { errorType } (without details.method) will stop matching.

Most producers/fixtures already include details.method for totp-required/totp-invalid/totp-max-attempts (e.g., packages/api-client/__tests__/2fahandling.spec.ts, apps/meteor/client/lib/2fa/challenge2fa.spec.tsx, and apps/meteor/tests/e2e/account-totp.spec.ts). The remaining legacy-shaped case found is packages/ddp-client/__tests__/Account.spec.ts emitting pageLoadLogin with { error: 'totp-required' } (no details); confirm this payload shape is not what any runtime code feeds into the isTotp*Error predicates.

Comment on lines +189 to +202
it('rejects the code promise when the dialog is closed via the X button', async () => {
setup();

let result: ReturnType<typeof challenge2fa>;
act(() => {
result = challenge2fa({ error });
});
const [code] = result!;

expect(await screen.findByRole('dialog')).toBeInTheDocument();
await Promise.all([
userEvent.click(screen.getByRole('button', { name: 'Cancel' })),
expect(code).rejects.toThrow('Two-factor_authentication_cancelled'),
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This doesn't exercise the close-icon path.

Line 200 clicks the same Cancel button as the previous test, so regressions in the modal's X-button wiring will still pass here. Please query the actual dismiss control for this case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/2fa/challenge2fa.spec.tsx` around lines 189 - 202, The
test "rejects the code promise when the dialog is closed via the X button"
currently clicks the same Cancel button; update the test to click the dialog's
dismiss (close-icon) control instead so the X-button path is exercised: after
calling challenge2fa(...) and asserting the dialog is present, target the close
icon via an accessible query such as screen.getByRole('button', { name: /close/i
}) or screen.getByLabelText('Close') (whichever matches the modal
implementation) and click that, then assert the code promise rejects with
'Two-factor_authentication_cancelled'; keep the rest of the test and references
to challenge2fa unchanged.

Comment on lines +143 to +150
if (unresolvedChallenge) {
// This is a retry, the modal will catch this error in order to show inline information
unresolvedChallenge.rejectChallenge(error);
saveChallenge({ onConfirm, rejectChallenge, onClose, rejectCode });
return [code, wrapWithEndChallenge(resolveChallenge)];
}

saveChallenge({ onConfirm, rejectChallenge, onClose, rejectCode });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Overwriting unresolvedChallenge can strand the first caller.

packages/api-client/src/index.ts awaits the returned code promise per challenged request. If a second request hits this branch before the user submits the first modal, you replace the stored callbacks and return a new promise, but the first caller is still waiting on its old code promise, which is no longer reachable and can hang forever. Please queue concurrent challenges or explicitly reuse/cancel the existing tuple instead of overwriting it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/client/lib/2fa/challenge2fa.ts` around lines 143 - 150, The
current branch overwrites the stored unresolvedChallenge (used by saveChallenge)
which can leave the first caller's code promise unresolved; modify the logic in
challenge2fa.ts around unresolvedChallenge, saveChallenge, wrapWithEndChallenge,
resolveChallenge and rejectChallenge so concurrent challenges are not lost —
either implement a simple FIFO queue of pending challenge tuples and enqueue the
new {onConfirm, rejectChallenge, onClose, rejectCode} when unresolvedChallenge
exists, or explicitly reuse/cancel the existing tuple by rejecting the prior
unresolvedChallenge (calling its rejectChallenge with a distinct cancellation
error) before replacing it; ensure returned promises map to the correct queued
tuple (do not overwrite the previously stored callbacks without
resolving/rejecting them).

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/meteor/client/lib/2fa/challenge2fa.spec.tsx">

<violation number="1" location="apps/meteor/client/lib/2fa/challenge2fa.spec.tsx:151">
P3: This test claims to exercise the X-button dismiss path but clicks the same `Cancel` button as the preceding test. It won't catch regressions in the modal's close-icon wiring. Query the actual dismiss/close control (e.g., via an aria-label like `'Close'`) instead.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

expect(screen.getByText(title)).toBeInTheDocument();

await Promise.all([
userEvent.click(screen.getByRole('button', { name: 'Cancel' })),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This test claims to exercise the X-button dismiss path but clicks the same Cancel button as the preceding test. It won't catch regressions in the modal's close-icon wiring. Query the actual dismiss/close control (e.g., via an aria-label like 'Close') instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/lib/2fa/challenge2fa.spec.tsx, line 151:

<comment>This test claims to exercise the X-button dismiss path but clicks the same `Cancel` button as the preceding test. It won't catch regressions in the modal's close-icon wiring. Query the actual dismiss/close control (e.g., via an aria-label like `'Close'`) instead.</comment>

<file context>
@@ -0,0 +1,253 @@
+				expect(screen.getByText(title)).toBeInTheDocument();
+
+				await Promise.all([
+					userEvent.click(screen.getByRole('button', { name: 'Cancel' })),
+					expect(code).rejects.toThrow('Two-factor_authentication_cancelled'),
+				]);
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The close button runs the same callback as the cancel button. I can rename the test though

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for clarifying—if both controls share the same callback, renaming the test to reflect that behavior sounds good.

@gabriellsh

Copy link
Copy Markdown
Member Author

Not ideal and too much for a regression.

@gabriellsh gabriellsh closed this May 28, 2026
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.

1 participant