Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: imported fixes #33202

Merged
merged 1 commit into from
Sep 3, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/orange-clocks-wait.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)
20 changes: 20 additions & 0 deletions apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const getModifiedHttpHeaders = (httpHeaders: Record<string, any>) => {
const modifiedHttpHeaders = { ...httpHeaders };

if ('x-auth-token' in modifiedHttpHeaders) {
modifiedHttpHeaders['x-auth-token'] = '[redacted]';
}

if (modifiedHttpHeaders.cookie) {
const cookies = modifiedHttpHeaders.cookie.split('; ');
const modifiedCookies = cookies.map((cookie: string) => {
if (cookie.startsWith('rc_token=')) {
return 'rc_token=[redacted]';
}
return cookie;
});
modifiedHttpHeaders.cookie = modifiedCookies.join('; ');
}

return modifiedHttpHeaders;
};
3 changes: 2 additions & 1 deletion apps/meteor/app/lib/server/lib/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { getMethodArgs } from '../../../../server/lib/logger/logPayloads';
import { metrics } from '../../../metrics/server';
import { settings } from '../../../settings/server';
import { getModifiedHttpHeaders } from '../functions/getModifiedHttpHeaders';

const logger = new Logger('Meteor');

Expand Down Expand Up @@ -41,7 +42,7 @@
console.log(name, {
id: connection.id,
clientAddress: connection.clientAddress,
httpHeaders: connection.httpHeaders,
httpHeaders: getModifiedHttpHeaders(connection.httpHeaders),
userId,
});
} else {
Expand Down Expand Up @@ -80,7 +81,7 @@
const originalMeteorMethods = Meteor.methods;

Meteor.methods = function (methodMap) {
_.each(methodMap, (handler, name) => {

Check warning on line 84 in apps/meteor/app/lib/server/lib/debug.js

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Consider using the native Array.prototype.forEach() or Object.entries().forEach()
wrapMethods(name, handler, methodMap);
});
originalMeteorMethods(methodMap);
Expand Down
107 changes: 58 additions & 49 deletions apps/meteor/app/livechat/server/api/v1/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,61 +31,70 @@ import { findVisitorInfo } from '../lib/visitors';

const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj);

API.v1.addRoute('livechat/room', {
async get() {
// I'll temporary use check for validation, as validateParams doesnt support what's being done here
const extraCheckParams = await onCheckRoomParams({
token: String,
rid: Match.Maybe(String),
agentId: Match.Maybe(String),
});

check(this.queryParams, extraCheckParams as any);

const { token, rid: roomId, agentId, ...extraParams } = this.queryParams;

const guest = token && (await findGuest(token));
if (!guest) {
throw new Error('invalid-token');
}

let room: IOmnichannelRoom | null;
if (!roomId) {
room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
if (room) {
return API.v1.success({ room, newRoom: false });
}

let agent: SelectedAgent | undefined;
const agentObj = agentId && (await findAgent(agentId));
if (agentObj) {
if (isAgentWithInfo(agentObj)) {
const { username = undefined } = agentObj;
agent = { agentId, username };
} else {
agent = { agentId };
}
API.v1.addRoute(
'livechat/room',
{
rateLimiterOptions: {
numRequestsAllowed: 5,
intervalTimeInMS: 60000,
},
},
{
async get() {
// I'll temporary use check for validation, as validateParams doesnt support what's being done here
const extraCheckParams = await onCheckRoomParams({
token: String,
rid: Match.Maybe(String),
agentId: Match.Maybe(String),
});

check(this.queryParams, extraCheckParams as any);

const { token, rid: roomId, agentId, ...extraParams } = this.queryParams;

const guest = token && (await findGuest(token));
if (!guest) {
throw new Error('invalid-token');
}

const rid = Random.id();
const roomInfo = {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
},
};
let room: IOmnichannelRoom | null;
if (!roomId) {
room = await LivechatRooms.findOneOpenByVisitorToken(token, {});
if (room) {
return API.v1.success({ room, newRoom: false });
}

const newRoom = await getRoom({ guest, rid, agent, roomInfo, extraParams });
return API.v1.success(newRoom);
}
let agent: SelectedAgent | undefined;
const agentObj = agentId && (await findAgent(agentId));
if (agentObj) {
if (isAgentWithInfo(agentObj)) {
const { username = undefined } = agentObj;
agent = { agentId, username };
} else {
agent = { agentId };
}
}

const rid = Random.id();
const roomInfo = {
source: {
type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API,
},
};

const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token, {});
if (!froom) {
throw new Error('invalid-room');
}
const newRoom = await getRoom({ guest, rid, agent, roomInfo, extraParams });
return API.v1.success(newRoom);
}

return API.v1.success({ room: froom, newRoom: false });
const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token, {});
if (!froom) {
throw new Error('invalid-room');
}

return API.v1.success({ room: froom, newRoom: false });
},
},
});
);

// Note: use this route if a visitor is closing a room
// If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route
Expand Down
211 changes: 110 additions & 101 deletions apps/meteor/app/livechat/server/api/v1/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,117 +9,126 @@ import { settings } from '../../../../settings/server';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { findGuest, normalizeHttpHeaderData } from '../lib/livechat';

API.v1.addRoute('livechat/visitor', {
async post() {
check(this.bodyParams, {
visitor: Match.ObjectIncluding({
token: String,
name: Match.Maybe(String),
email: Match.Maybe(String),
department: Match.Maybe(String),
phone: Match.Maybe(String),
username: Match.Maybe(String),
customFields: Match.Maybe([
Match.ObjectIncluding({
key: String,
value: String,
overwrite: Boolean,
}),
]),
}),
});
API.v1.addRoute(
'livechat/visitor',
{
rateLimiterOptions: {
numRequestsAllowed: 5,
intervalTimeInMS: 60000,
},
},
{
async post() {
check(this.bodyParams, {
visitor: Match.ObjectIncluding({
token: String,
name: Match.Maybe(String),
email: Match.Maybe(String),
department: Match.Maybe(String),
phone: Match.Maybe(String),
username: Match.Maybe(String),
customFields: Match.Maybe([
Match.ObjectIncluding({
key: String,
value: String,
overwrite: Boolean,
}),
]),
}),
});

const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor;
const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor;

if (!token?.trim()) {
throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' });
}
if (!token?.trim()) {
throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' });
}

const guest = {
token,
...(id && { id }),
...(name && { name }),
...(email && { email }),
...(department && { department }),
...(username && { username }),
...(connectionData && { connectionData }),
...(phone && typeof phone === 'string' && { phone: { number: phone as string } }),
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitorId = await LivechatTyped.registerGuest(guest);

let visitor: ILivechatVisitor | null = await VisitorsRaw.findOneEnabledById(visitorId, {});
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
// If it's updating an existing visitor, it must also update the roomInfo
const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
await Promise.all(
rooms.map(
(room: IRoom) =>
visitor &&
LivechatTyped.saveRoomInfo(room, {
_id: visitor._id,
name: visitor.name,
phone: visitor.phone?.[0]?.phoneNumber,
livechatData: visitor.livechatData as { [k: string]: string },
}),
),
);
}
const guest = {
token,
...(id && { id }),
...(name && { name }),
...(email && { email }),
...(department && { department }),
...(username && { username }),
...(connectionData && { connectionData }),
...(phone && typeof phone === 'string' && { phone: { number: phone as string } }),
connectionData: normalizeHttpHeaderData(this.request.headers),
};

const visitorId = await LivechatTyped.registerGuest(guest);

let visitor: ILivechatVisitor | null = await VisitorsRaw.findOneEnabledById(visitorId, {});
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
// If it's updating an existing visitor, it must also update the roomInfo
const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray();
await Promise.all(
rooms.map(
(room: IRoom) =>
visitor &&
LivechatTyped.saveRoomInfo(room, {
_id: visitor._id,
name: visitor.name,
phone: visitor.phone?.[0]?.phoneNumber,
livechatData: visitor.livechatData as { [k: string]: string },
}),
),
);
}

if (customFields && Array.isArray(customFields) && customFields.length > 0) {
const keys = customFields.map((field) => field.key);
const errors: string[] = [];

if (customFields && Array.isArray(customFields) && customFields.length > 0) {
const keys = customFields.map((field) => field.key);
const errors: string[] = [];

const processedKeys = await Promise.all(
await LivechatCustomField.findByIdsAndScope<Pick<ILivechatCustomField, '_id'>>(keys, 'visitor', {
projection: { _id: 1 },
})
.map(async (field) => {
const customField = customFields.find((f) => f.key === field._id);
if (!customField) {
return;
}

const { key, value, overwrite } = customField;
// TODO: Change this to Bulk update
if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) {
errors.push(key);
}

return key;
const processedKeys = await Promise.all(
await LivechatCustomField.findByIdsAndScope<Pick<ILivechatCustomField, '_id'>>(keys, 'visitor', {
projection: { _id: 1 },
})
.toArray(),
);

if (processedKeys.length !== keys.length) {
LivechatTyped.logger.warn({
msg: 'Some custom fields were not processed',
visitorId,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
.map(async (field) => {
const customField = customFields.find((f) => f.key === field._id);
if (!customField) {
return;
}

const { key, value, overwrite } = customField;
// TODO: Change this to Bulk update
if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) {
errors.push(key);
}

return key;
})
.toArray(),
);

if (processedKeys.length !== keys.length) {
LivechatTyped.logger.warn({
msg: 'Some custom fields were not processed',
visitorId,
missingKeys: keys.filter((key) => !processedKeys.includes(key)),
});
}

if (errors.length > 0) {
LivechatTyped.logger.error({
msg: 'Error updating custom fields',
visitorId,
errors,
});
throw new Error('error-updating-custom-fields');
}

visitor = await VisitorsRaw.findOneEnabledById(visitorId, {});
}

if (errors.length > 0) {
LivechatTyped.logger.error({
msg: 'Error updating custom fields',
visitorId,
errors,
});
throw new Error('error-updating-custom-fields');
if (!visitor) {
throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor');
}

visitor = await VisitorsRaw.findOneEnabledById(visitorId, {});
}

if (!visitor) {
throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor');
}

return API.v1.success({ visitor });
return API.v1.success({ visitor });
},
},
});
);

API.v1.addRoute('livechat/visitor/:token', {
async get() {
Expand Down
Loading
Loading