Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 52 additions & 26 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
isRoomsImagesProps,
isRoomsMuteUnmuteUserProps,
isRoomsExportProps,
isRoomsIsMemberProps,
isRoomsCleanHistoryProps,
isRoomsOpenProps,
isRoomsMembersOrderedByRoleProps,
Expand All @@ -18,6 +17,7 @@ import {
isRoomsInviteProps,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
validateForbiddenErrorResponse,
} from '@rocket.chat/rest-typings';
import { isTruthy } from '@rocket.chat/tools';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -792,34 +792,60 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
const isMemeberEndpoint = API.v1.get(
'rooms.isMember',
{
authRequired: true,
validateParams: isRoomsIsMemberProps,
query: ajv.compile<{ roomId: string; userId?: string; username?: string }>({
type: 'object',
properties: {
roomId: { type: 'string' },
userId: { type: 'string' },
username: { type: 'string' },
},
oneOf: [{ required: ['roomId', 'userId'] }, { required: ['roomId', 'username'] }],
additionalProperties: false,
}),
response: {
200: ajv.compile<{ isMember: boolean }>({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [true],
description: 'Indicates if the request was successful.',
},
isMember: {
type: 'boolean',
},
},
required: ['success', 'isMember'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
{
async get() {
const { roomId, userId, username } = this.queryParams;
const [room, user] = await Promise.all([
findRoomByIdOrName({
params: { roomId },
}) as Promise<IRoom>,
Users.findOneByIdOrUsername(userId || username),
]);

if (!user?._id) {
return API.v1.failure('error-user-not-found');
}
async function action() {
const { roomId, userId, username } = this.queryParams;
const room = await findRoomByIdOrName({
params: { roomId },
});

if (await canAccessRoomAsync(room, { _id: this.user._id })) {
return API.v1.success({
isMember: (await Subscriptions.countByRoomIdAndUserId(room._id, user._id)) > 0,
});
}
return API.v1.forbidden();
},
if (!(await canAccessRoomAsync(room, { _id: this.userId }))) {
return API.v1.forbidden('unauthorized');
}

const user = await Users.findOneByIdOrUsername((userId || username)!);

if (!user?._id) {
return API.v1.failure('error-user-not-found');
}

return API.v1.success({
isMember: (await Subscriptions.countByRoomIdAndUserId(room._id, user._id)) > 0,
});
Comment on lines +831 to +848
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.

why did you change the action logic ?

Copy link
Copy Markdown
Contributor Author

@Rohitgiri02 Rohitgiri02 Mar 9, 2026

Choose a reason for hiding this comment

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

suggested by code rabbit to prevent ⚠️ Potential issue | 🟠 Major

Check room access before resolving the target user. that was a security risk

An authenticated caller who cannot access the room can currently distinguish error-user-not-found from the forbidden path, because the user lookup runs before the room-access guard. That leaks user existence and makes one unauthorized branch return 400 instead of 403.

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.

suggested by code rabbit to prevent ⚠️ Potential issue | 🟠 Major

Check room access before resolving the target user. that was a security risk

An authenticated caller who cannot access the room can currently distinguish error-user-not-found from the forbidden path, because the user lookup runs before the room-access guard. That leaks user existence and makes one unauthorized branch return 400 instead of 403.

was it working before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes it was working before, but code rabbit proposed this change which actually makes sense

},
);

Expand Down Expand Up @@ -1236,9 +1262,9 @@ export const roomEndpoints = API.v1
);

type RoomEndpoints = ExtractRoutesFromAPI<typeof roomEndpoints> &
ExtractRoutesFromAPI<typeof roomEndpoints> &
ExtractRoutesFromAPI<typeof roomDeleteEndpoint> &
ExtractRoutesFromAPI<typeof roomsSaveNotificationEndpoint>;
ExtractRoutesFromAPI<typeof roomsSaveNotificationEndpoint> &
ExtractRoutesFromAPI<typeof isMemeberEndpoint>;
Comment on lines 1264 to +1267
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.

replace these with only type RoomEndpoints = ExtractRoutesFromAPI<typeof roomEndpoints> and chaining the APIs together

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok


declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
Expand Down
19 changes: 0 additions & 19 deletions packages/rest-typings/src/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,21 +458,6 @@ const GETRoomsNameExistsSchema = {

export const isGETRoomsNameExists = ajv.compile<GETRoomsNameExists>(GETRoomsNameExistsSchema);

type RoomsIsMemberProps = { roomId: string } & ({ username: string } | { userId: string });

const RoomsIsMemberPropsSchema = {
type: 'object',
properties: {
roomId: { type: 'string', minLength: 1 },
userId: { type: 'string', minLength: 1 },
username: { type: 'string', minLength: 1 },
},
oneOf: [{ required: ['roomId', 'userId'] }, { required: ['roomId', 'username'] }],
additionalProperties: false,
};

export const isRoomsIsMemberProps = ajv.compile<RoomsIsMemberProps>(RoomsIsMemberPropsSchema);

export type Notifications = {
disableNotifications?: string;
muteGroupMentions?: string;
Expand Down Expand Up @@ -819,10 +804,6 @@ export type RoomsEndpoints = {
}>;
};

'/v1/rooms.isMember': {
GET: (params: RoomsIsMemberProps) => { isMember: boolean };
};

'/v1/rooms.muteUser': {
POST: (params: RoomsMuteUnmuteUser) => void;
};
Expand Down