fix: imported fixes 2025-12-19 (#37880)
Some checks failed
Code scanning - action / CodeQL-Build (push) Has been cancelled

Co-authored-by: Julio Araujo <julio.araujo@rocket.chat>
This commit is contained in:
dionisio-bot[bot] 2025-12-22 22:58:40 -03:00 committed by GitHub
parent 2356ad7124
commit 45b1c4e646
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 46 additions and 15 deletions

View File

@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---
Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)

View File

@ -23,16 +23,22 @@ export const isValidQuery: {
const verifyQuery = (query: Query, allowedAttributes: string[], allowedOperations: string[], parent = ''): boolean => {
return Object.entries(removeDangerousProps(query)).every(([key, value]) => {
const path = parent ? `${parent}.${key}` : key;
if (parent === '' && path.startsWith('$')) {
if (!allowedOperations.includes(path)) {
isValidQuery.errors.push(`Invalid operation: ${path}`);
if (key.startsWith('$')) {
if (!allowedOperations.includes(key)) {
isValidQuery.errors.push(`Invalid operation: ${key}`);
return false;
}
if (!Array.isArray(value)) {
isValidQuery.errors.push(`Invalid parameter for operation: ${path} : ${value}`);
return false;
if (Array.isArray(value)) {
return value.every((v) => verifyQuery(v, allowedAttributes, allowedOperations));
}
return value.every((v) => verifyQuery(v, allowedAttributes, allowedOperations));
if (value instanceof Object) {
return verifyQuery(value, allowedAttributes, allowedOperations, path);
}
// handles primitive values (strings, numbers, booleans, etc.)
return true;
}
if (

View File

@ -51,7 +51,7 @@ import {
} from '../../../lib/server/functions/checkUsernameAvailability';
import { deleteUser } from '../../../lib/server/functions/deleteUser';
import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser';
import { getFullUserDataByIdOrUsernameOrImportId } from '../../../lib/server/functions/getFullUserData';
import { getFullUserDataByIdOrUsernameOrImportId, defaultFields, fullFields } from '../../../lib/server/functions/getFullUserData';
import { generateUsernameSuggestion } from '../../../lib/server/functions/getUsernameSuggestion';
import { saveCustomFields } from '../../../lib/server/functions/saveCustomFields';
import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions/saveCustomFieldsWithoutValidation';
@ -491,13 +491,18 @@ API.v1.addRoute(
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, fields, query } = await this.parseJsonQuery();
const nonEmptyQuery = getNonEmptyQuery(query, await hasPermissionAsync(this.userId, 'view-full-other-user-info'));
const nonEmptyFields = getNonEmptyFields(fields);
const inclusiveFields = getInclusiveFields(nonEmptyFields);
const inclusiveFieldsKeys = Object.keys(inclusiveFields);
const hasUserQuery = query && Object.keys(query).length > 0;
const nonEmptyQuery = getNonEmptyQuery(query, await hasPermissionAsync(this.userId, 'view-full-other-user-info'));
// if user provided a query, validate it with their allowed operators
// otherwise we use the default query (with $regex and $options)
if (
!isValidQuery(
nonEmptyQuery,
@ -509,7 +514,7 @@ API.v1.addRoute(
inclusiveFieldsKeys.includes('type') && 'type.*',
inclusiveFieldsKeys.includes('customFields') && 'customFields.*',
].filter(Boolean) as string[],
this.queryOperations,
hasUserQuery ? this.queryOperations : [...this.queryOperations, '$regex', '$options'],
)
) {
throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n'));
@ -1117,8 +1122,13 @@ API.v1.addRoute(
const selector: { exceptions: Required<IUser>['username'][]; conditions: Filter<IUser>; term: string } = JSON.parse(selectorRaw);
try {
if (selector?.conditions && !isValidQuery(selector.conditions, ['*'], ['$or', '$and'])) {
throw new Error('error-invalid-query');
if (selector?.conditions) {
const canViewFullInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info');
const allowedFields = canViewFullInfo ? [...Object.keys(defaultFields), ...Object.keys(fullFields)] : Object.keys(defaultFields);
if (!isValidQuery(selector.conditions, allowedFields, ['$and', '$ne', '$exists'])) {
throw new Error('error-invalid-query');
}
}
} catch (e) {
return API.v1.failure(e);

View File

@ -7,7 +7,7 @@ import { settings } from '../../../settings/server';
const logger = new Logger('getFullUserData');
const defaultFields = {
export const defaultFields = {
name: 1,
username: 1,
nickname: 1,
@ -24,7 +24,7 @@ const defaultFields = {
statusLivechat: 1,
} as const;
const fullFields = {
export const fullFields = {
emails: 1,
phone: 1,
statusConnection: 1,

View File

@ -174,7 +174,7 @@ describe('isValidQuery', () => {
},
},
};
expect(isValidQuery(query, props, ['$or'])).to.be.true;
expect(isValidQuery(query, props, ['$or', '$regex'])).to.be.true;
expect(isValidQuery.errors.length).to.be.equals(0);
});
@ -212,5 +212,15 @@ describe('isValidQuery', () => {
),
).to.be.false;
});
it('should return false if the query contains nested conditions with disallowed operators', () => {
const props = ['username'];
const allowedOps = ['$and', '$ne'];
const query = {
$and: [{ username: { $exists: true } }, { username: { $ne: '1000' } }],
};
expect(isValidQuery(query, props, allowedOps)).to.be.false;
expect(isValidQuery.errors.length).to.be.equals(1);
});
});
});