Skip to content

Commit

Permalink
Merge pull request #32816 from RocketChat/release-6.10.1
Browse files Browse the repository at this point in the history
Release 6.10.1
  • Loading branch information
sampaiodiego authored Jul 25, 2024
2 parents 623a265 + ce072a1 commit e9c90f6
Show file tree
Hide file tree
Showing 39 changed files with 962 additions and 142 deletions.
5 changes: 5 additions & 0 deletions .changeset/bump-patch-1721226485501.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Bump @rocket.chat/meteor version.
5 changes: 5 additions & 0 deletions .changeset/happy-peaches-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixed issue with livechat agents not being able to leave omnichannel rooms if joining after a room has been closed by the visitor (due to race conditions)
5 changes: 5 additions & 0 deletions .changeset/hungry-jars-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)
5 changes: 5 additions & 0 deletions .changeset/thin-windows-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes an issue not displaying all groups in settings list
5 changes: 5 additions & 0 deletions .changeset/violet-brooms-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)
4 changes: 4 additions & 0 deletions apps/meteor/app/2fa/server/methods/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Meteor.methods<ServerMethods>({
});
}

if (user.services?.totp?.enabled) {
throw new Meteor.Error('error-2fa-already-enabled');
}

const secret = TOTP.generateSecret();

await Users.disable2FAAndSetTempSecretByUserId(userId, secret.base32);
Expand Down
5 changes: 5 additions & 0 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { executeUpdateMessage } from '../../../lib/server/methods/updateMessage'
import { OEmbed } from '../../../oembed/server/server';
import { executeSetReaction } from '../../../reactions/server/setReaction';
import { settings } from '../../../settings/server';
import { MessageTypes } from '../../../ui-utils/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';
Expand Down Expand Up @@ -217,6 +218,10 @@ API.v1.addRoute(
throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.');
}

if (MessageTypes.isSystemMessage(this.bodyParams.message)) {
throw new Error("Cannot send system messages using 'chat.sendMessage'");
}

const sent = await executeSendMessage(this.userId, this.bodyParams.message as Pick<IMessage, 'rid'>, this.bodyParams.previewUrls);
const [message] = await normalizeMessagesForUser([sent], this.userId);

Expand Down
21 changes: 19 additions & 2 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { setStatusText } from '../../../lib/server/functions/setStatusText';
import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar';
import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername';
import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields';
import { validateNameChars } from '../../../lib/server/functions/validateNameChars';
import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener';
import { generateAccessToken } from '../../../lib/server/methods/createToken';
import { settings } from '../../../settings/server';
Expand Down Expand Up @@ -94,6 +95,10 @@ API.v1.addRoute(
async post() {
const userData = { _id: this.bodyParams.userId, ...this.bodyParams.data };

if (userData.name && !validateNameChars(userData.name)) {
return API.v1.failure('Name contains invalid characters');
}

await saveUser(this.userId, userData);

if (this.bodyParams.data.customFields) {
Expand Down Expand Up @@ -138,6 +143,10 @@ API.v1.addRoute(
typedPassword: this.bodyParams.data.currentPassword,
};

if (userData.realname && !validateNameChars(userData.realname)) {
return API.v1.failure('Name contains invalid characters');
}

// saveUserProfile now uses the default two factor authentication procedures, so we need to provide that
const twoFactorOptions = !userData.typedPassword
? null
Expand Down Expand Up @@ -280,6 +289,10 @@ API.v1.addRoute(
this.bodyParams.joinDefaultChannels = true;
}

if (this.bodyParams.name && !validateNameChars(this.bodyParams.name)) {
return API.v1.failure('Name contains invalid characters');
}

if (this.bodyParams.customFields) {
validateCustomFields(this.bodyParams.customFields);
}
Expand Down Expand Up @@ -627,16 +640,20 @@ API.v1.addRoute(
},
{
async post() {
const { secret: secretURL, ...params } = this.bodyParams;

if (this.userId) {
return API.v1.failure('Logged in users can not register again.');
}

if (params.name && !validateNameChars(params.name)) {
return API.v1.failure('Name contains invalid characters');
}

if (!(await checkUsernameAvailability(this.bodyParams.username))) {
return API.v1.failure('Username is already in use');
}

const { secret: secretURL, ...params } = this.bodyParams;

if (this.bodyParams.customFields) {
try {
await validateCustomFields(this.bodyParams.customFields);
Expand Down
1 change: 0 additions & 1 deletion apps/meteor/app/discussion/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
// Other UI extensions
import './lib/messageTypes/discussionMessage';
import './createDiscussionMessageAction';

This file was deleted.

100 changes: 100 additions & 0 deletions apps/meteor/app/lib/server/functions/checkUrlForSsrf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { lookup } from 'dns';

// https://en.wikipedia.org/wiki/Reserved_IP_addresses + Alibaba Metadata IP
const ranges: string[] = [
'0.0.0.0/8',
'10.0.0.0/8',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.0.0.0/24',
'192.0.2.0/24',
'192.88.99.0/24',
'192.168.0.0/16',
'198.18.0.0/15',
'198.51.100.0/24',
'203.0.113.0/24',
'224.0.0.0/4',
'240.0.0.0/4',
'255.255.255.255',
'100.100.100.200/32',
];

export const nslookup = async (hostname: string): Promise<string> => {
return new Promise((resolve, reject) => {
lookup(hostname, (error, address) => {
if (error) {
reject(error);
} else {
resolve(address);
}
});
});
};

export const ipToLong = (ip: string): number => {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
};

export const isIpInRange = (ip: string, range: string): boolean => {
const [rangeIp, subnet] = range.split('/');
const ipLong = ipToLong(ip);
const rangeIpLong = ipToLong(rangeIp);
const mask = ~(2 ** (32 - Number(subnet)) - 1);
return (ipLong & mask) === (rangeIpLong & mask);
};

export const isIpInAnyRange = (ip: string): boolean => ranges.some((range) => isIpInRange(ip, range));

export const isValidIPv4 = (ip: string): boolean => {
const octets = ip.split('.');
if (octets.length !== 4) return false;
return octets.every((octet) => {
const num = Number(octet);
return num >= 0 && num <= 255 && octet === num.toString();
});
};

export const isValidDomain = (domain: string): boolean => {
const domainPattern = /^(?!-)(?!.*--)[A-Za-z0-9-]{1,63}(?<!-)\.?([A-Za-z]{2,63}\.?)*[A-Za-z]{2,63}$/;
if (!domainPattern.test(domain)) {
return false;
}
return true;
};

export const checkUrlForSsrf = async (url: string): Promise<boolean> => {
if (!(url.startsWith('http://') || url.startsWith('https://'))) {
return false;
}

const [, address] = url.split('://');
const ipOrDomain = address.includes('/') ? address.split('/')[0] : address;

if (!(isValidIPv4(ipOrDomain) || isValidDomain(ipOrDomain))) {
return false;
}

if (isValidIPv4(ipOrDomain) && isIpInAnyRange(ipOrDomain)) {
return false;
}

if (isValidDomain(ipOrDomain) && /metadata.google.internal/.test(ipOrDomain.toLowerCase())) {
return false;
}

if (isValidDomain(ipOrDomain)) {
try {
const ipAddress = await nslookup(ipOrDomain);
if (isIpInAnyRange(ipAddress)) {
return false;
}
} catch (error) {
console.log(error);
return false;
}
}

return true;
};
81 changes: 81 additions & 0 deletions apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped';
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';

export const closeLivechatRoom = async (
user: IUser,
roomId: IRoom['_id'],
{
comment,
tags,
generateTranscriptPdf,
transcriptEmail,
}: {
comment?: string;
tags?: string[];
generateTranscriptPdf?: boolean;
transcriptEmail?:
| {
sendToVisitor: false;
}
| {
sendToVisitor: true;
requestData: Pick<NonNullable<IOmnichannelRoom['transcriptRequest']>, 'email' | 'subject'>;
};
},
): Promise<void> => {
const room = await LivechatRooms.findOneById(roomId);
if (!room || !isOmnichannelRoom(room)) {
throw new Error('error-invalid-room');
}

if (!room.open) {
const subscriptionsLeft = await Subscriptions.countByRoomId(roomId);
if (subscriptionsLeft) {
await Subscriptions.removeByRoomId(roomId);
return;
}
throw new Error('error-room-already-closed');
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } });
if (!subscription && !(await hasPermissionAsync(user._id, 'close-others-livechat-room'))) {
throw new Error('error-not-authorized');
}

const options: CloseRoomParams['options'] = {
clientAction: true,
tags,
...(generateTranscriptPdf && { pdfTranscript: { requestedBy: user._id } }),
...(transcriptEmail && {
...(transcriptEmail.sendToVisitor
? {
emailTranscript: {
sendToVisitor: true,
requestData: {
email: transcriptEmail.requestData.email,
subject: transcriptEmail.requestData.subject,
requestedAt: new Date(),
requestedBy: user,
},
},
}
: {
emailTranscript: {
sendToVisitor: false,
},
}),
}),
};

await Livechat.closeRoom({
room,
user,
options,
comment,
});
};
7 changes: 7 additions & 0 deletions apps/meteor/app/lib/server/functions/saveUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ async function _sendUserEmail(subject, html, userData) {
async function validateUserData(userId, userData) {
const existingRoles = _.pluck(await getRoles(), '_id');

if (userData.verified && userData._id && userId === userData._id) {
throw new Meteor.Error('error-action-not-allowed', 'Editing email verification is not allowed', {
method: 'insertOrUpdateUser',
action: 'Editing_user',
});
}

if (userData._id && userId !== userData._id && !(await hasPermissionAsync(userId, 'edit-other-user-info'))) {
throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed', {
method: 'insertOrUpdateUser',
Expand Down
12 changes: 11 additions & 1 deletion apps/meteor/app/lib/server/functions/setUserAvatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP
import { FileUpload } from '../../../file-upload/server';
import { RocketChatFile } from '../../../file/server';
import { settings } from '../../../settings/server';
import { checkUrlForSsrf } from './checkUrlForSsrf';

export const setAvatarFromServiceWithValidation = async (
userId: string,
Expand Down Expand Up @@ -88,8 +89,17 @@ export async function setUserAvatar(
const { buffer, type } = await (async (): Promise<{ buffer: Buffer; type: string }> => {
if (service === 'url' && typeof dataURI === 'string') {
let response: Response;

const isSsrfSafe = await checkUrlForSsrf(dataURI);
if (!isSsrfSafe) {
throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${encodeURI(dataURI)}`, {
function: 'setUserAvatar',
url: dataURI,
});
}

try {
response = await fetch(dataURI);
response = await fetch(dataURI, { redirect: 'error' });
} catch (e) {
SystemLogger.info(`Not a valid response, from the avatar url: ${encodeURI(dataURI)}`);
throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${encodeURI(dataURI)}`, {
Expand Down
21 changes: 21 additions & 0 deletions apps/meteor/app/lib/server/functions/validateNameChars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const validateNameChars = (name: string | undefined): boolean => {
if (typeof name !== 'string') {
return false;
}

const invalidChars = /[<>\\/]/;
if (invalidChars.test(name)) {
return false;
}

try {
const decodedName = decodeURI(name);
if (invalidChars.test(decodedName)) {
return false;
}
} catch (err) {
return false;
}

return true;
};
Loading

0 comments on commit e9c90f6

Please sign in to comment.