Skip to content

Commit e7bf767

Browse files
dionisio-bot[bot]julio-rocketchatricardogarimpierre-lehnen-rc
authored
fix: imported fixes 04-04-26 (#40411)
Co-authored-by: Julio Araujo <julio.araujo@rocket.chat> Co-authored-by: Ricardo Garim <rswarovsky@gmail.com> Co-authored-by: Pierre Lehnen <pierre.lehnen@rocket.chat>
1 parent 0d1c565 commit e7bf767

42 files changed

Lines changed: 1061 additions & 157 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/hot-spoons-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
---
4+
5+
Disables SAML login when it is set to validate signatures without the proper configuration for it

.changeset/rich-doodles-grow.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@rocket.chat/model-typings': patch
3+
'@rocket.chat/core-typings': patch
4+
'@rocket.chat/models': patch
5+
'@rocket.chat/i18n': patch
6+
'@rocket.chat/meteor': patch
7+
---
8+
9+
Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)

apps/meteor/.mocharc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = {
1111
exit: true,
1212
spec: [
1313
'server/lib/callbacks.spec.ts',
14+
'server/lib/cas/*.spec.ts',
1415
'server/lib/ldap/*.spec.ts',
1516
'server/lib/ldap/**/*.spec.ts',
1617
'server/lib/dataExport/**/*.spec.ts',

apps/meteor/app/file-upload/server/lib/FileUpload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const defaults: Record<string, () => Partial<StoreOptions>> = {
9494
return {
9595
collection: UserDataFiles,
9696
getPath(file: IUpload) {
97-
return `${settings.get('uniqueID')}/uploads/userData/${file.userId}`;
97+
return `${settings.get('uniqueID')}/uploads/userData/${file.userId}/${file._id}`;
9898
},
9999
onValidate: FileUpload.uploadsOnValidate,
100100
async onRead(_fileId: string, file: IUpload, req: http.IncomingMessage, res: http.ServerResponse) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { expect } from 'chai';
2+
import { describe, it, beforeEach } from 'mocha';
3+
import proxyquire from 'proxyquire';
4+
import sinon from 'sinon';
5+
6+
const findOneByIdAndUserIdAndRoomId = sinon.stub();
7+
const updateFileMetadata = sinon.stub().resolves();
8+
const getPath = sinon.stub().returns('/path/to/file.txt');
9+
const isImagePreviewSupported = sinon.stub().returns(false);
10+
const getFileExtension = sinon.stub().returns('txt');
11+
12+
const { parseFileIntoMessageAttachments } = proxyquire.noCallThru().load('./sendFileMessage', {
13+
'@rocket.chat/models': {
14+
Uploads: { findOneByIdAndUserIdAndRoomId, updateFileMetadata },
15+
Rooms: { findOneById: sinon.stub() },
16+
Users: { findOneById: sinon.stub() },
17+
},
18+
'meteor/check': {
19+
check: sinon.stub(),
20+
Match: {
21+
Maybe: sinon.stub(),
22+
Optional: sinon.stub(),
23+
ObjectIncluding: sinon.stub(),
24+
},
25+
},
26+
'meteor/meteor': {
27+
Meteor: {
28+
Error: class Error extends global.Error {},
29+
methods: sinon.stub(),
30+
},
31+
},
32+
'../lib/FileUpload': {
33+
FileUpload: { getPath },
34+
},
35+
'./isImagePreviewSupported': { isImagePreviewSupported },
36+
'../../../../lib/utils/getFileExtension': { getFileExtension },
37+
'../../../../server/lib/callbacks': { callbacks: { runAsync: sinon.stub() } },
38+
'../../../../server/lib/logger/system': { SystemLogger: { error: sinon.stub() } },
39+
'../../../authorization/server/functions/canAccessRoom': { canAccessRoomAsync: sinon.stub().resolves(true) },
40+
'../../../lib/server/methods/sendMessage': { executeSendMessage: sinon.stub().resolves({}) },
41+
});
42+
43+
describe('sendFileMessage - Mass Assignment & Type Pollution Prevention', () => {
44+
const mockUser = { _id: 'user123' };
45+
const roomId = 'room123';
46+
47+
beforeEach(() => {
48+
findOneByIdAndUserIdAndRoomId.reset();
49+
updateFileMetadata.reset();
50+
51+
findOneByIdAndUserIdAndRoomId.resolves({ _id: 'file123' });
52+
});
53+
54+
it('should filter out invalid types, nulls, and malicious fields before updating the database', async () => {
55+
const maliciousFilePayload = {
56+
_id: 'file123',
57+
name: null, // invalid type, must be ignored
58+
type: 'text/plain',
59+
size: 1024,
60+
description: 12345, // invalid type, must be ignored
61+
typeGroup: 'image', // only valid field
62+
content: null, // invalid type, must be ignored
63+
maliciousRoleAssignment: 'admin', // mass assignment, must be ignored
64+
$set: { bypassSecurity: true }, // mongo injection, must be ignored
65+
};
66+
67+
await parseFileIntoMessageAttachments(maliciousFilePayload as any, roomId, mockUser as any);
68+
69+
expect(updateFileMetadata.calledOnce).to.equal(true);
70+
71+
const [fileId, userId, safeMetadata] = updateFileMetadata.getCall(0).args;
72+
73+
expect(fileId).to.equal('file123');
74+
expect(userId).to.equal('user123');
75+
76+
expect(safeMetadata).to.deep.equal({
77+
typeGroup: 'image',
78+
});
79+
});
80+
81+
it('should pass valid fields correctly to the database', async () => {
82+
const validFilePayload = {
83+
_id: 'file123',
84+
name: 'picture.jpg',
85+
type: 'image/jpeg',
86+
size: 2048,
87+
description: 'Description',
88+
typeGroup: 'image',
89+
content: {
90+
algorithm: 'rc.v1.aes-sha2',
91+
ciphertext: 'test',
92+
},
93+
};
94+
95+
await parseFileIntoMessageAttachments(validFilePayload as any, roomId, mockUser as any);
96+
97+
expect(updateFileMetadata.calledOnce).to.equal(true);
98+
99+
const [, , safeMetadata] = updateFileMetadata.getCall(0).args;
100+
101+
expect(safeMetadata).to.deep.equal({
102+
name: 'picture.jpg',
103+
description: 'Description',
104+
typeGroup: 'image',
105+
content: {
106+
algorithm: 'rc.v1.aes-sha2',
107+
ciphertext: 'test',
108+
},
109+
});
110+
});
111+
});

apps/meteor/app/file-upload/server/methods/sendFileMessage.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { Meteor } from 'meteor/meteor';
1515

1616
import { isImagePreviewSupported } from './isImagePreviewSupported';
1717
import { getFileExtension } from '../../../../lib/utils/getFileExtension';
18-
import { omit } from '../../../../lib/utils/omit';
1918
import { callbacks } from '../../../../server/lib/callbacks';
2019
import { SystemLogger } from '../../../../server/lib/logger/system';
2120
import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom';
@@ -45,7 +44,14 @@ export const parseFileIntoMessageAttachments = async (
4544
});
4645
}
4746

48-
await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id'));
47+
const safeMetadata = {
48+
...(typeof file.name === 'string' && { name: file.name }),
49+
...(typeof file.description === 'string' && { description: file.description }),
50+
...(typeof file.typeGroup === 'string' && { typeGroup: file.typeGroup }),
51+
...(file.content && typeof file.content === 'object' && { content: file.content }),
52+
};
53+
54+
await Uploads.updateFileMetadata(file._id, user._id, safeMetadata);
4955

5056
const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`);
5157

apps/meteor/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface IServiceProviderOptions {
1414
defaultUserRole: string;
1515
allowedClockDrift: number;
1616
signatureValidationType: string;
17+
validateLogoutRequestSignature: boolean;
18+
validateLogoutResponseSignature: boolean;
1719
identifierFormat: string;
1820
nameIDPolicyTemplate: string;
1921
authnContextTemplate: string;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Other binding values are not supported: HTTP-Artifact, URI, SOAP, PAOS
2+
export type SAMLBinding = 'HTTP-Redirect' | 'HTTP-POST';
3+
4+
export type SAMLDocumentType = 'SAMLRequest' | 'SAMLResponse';
5+
6+
export type SAMLBaseEnvelope<T extends SAMLDocumentType = SAMLDocumentType> = {
7+
binding: SAMLBinding;
8+
/**
9+
* encodedDocument is the document in the exact format we received it
10+
* */
11+
encodedDocument: string;
12+
/**
13+
* decodedDocument is the raw XML file; processed according to the binding specification
14+
* */
15+
decodedDocument: string;
16+
type: T;
17+
relayState?: string;
18+
};
19+
20+
/**
21+
* HTTP-Redirect envelope
22+
* For this binding, the document must be a Deflated XML file that is then base64 encoded
23+
**/
24+
export type SAMLRedirectEnvelope<T extends SAMLDocumentType = SAMLDocumentType> = SAMLBaseEnvelope<T> & {
25+
binding: 'HTTP-Redirect';
26+
sigAlg?: string;
27+
signature?: string;
28+
29+
/**
30+
* signedContent is the complete string value that must be matched by the signature
31+
* */
32+
signedContent?: string;
33+
};
34+
35+
/**
36+
* HTTP-POST envelope
37+
* For this binding, the document must be a regular XML file that is base64 encoded
38+
**/
39+
export type SAMLPOSTEnvelope<T extends SAMLDocumentType = SAMLDocumentType> = SAMLBaseEnvelope<T> & {
40+
binding: 'HTTP-POST';
41+
};
42+
43+
export type SAMLEnvelope<T extends SAMLDocumentType = SAMLDocumentType> = SAMLRedirectEnvelope<T> | SAMLPOSTEnvelope<T>;

apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Meteor } from 'meteor/meteor';
99

1010
import { SAMLServiceProvider } from './ServiceProvider';
1111
import { SAMLUtils } from './Utils';
12+
import { getSAMLEnvelope } from './getSAMLEnvelope';
1213
import { ensureArray } from '../../../../lib/utils/arrayUtils';
1314
import { SystemLogger } from '../../../../server/lib/logger/system';
1415
import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom';
@@ -283,11 +284,21 @@ export class SAML {
283284
}
284285

285286
private static async processLogoutRequest(req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions): Promise<void> {
287+
const errorHandler = (err: unknown) => {
288+
SystemLogger.error({ err });
289+
throw new Meteor.Error('Unable to Validate Logout Request');
290+
};
291+
292+
const envelope = await getSAMLEnvelope(req, 'SAMLRequest', 'HTTP-Redirect').catch(errorHandler);
293+
if (!envelope) {
294+
errorHandler('No envelope found for SAML Logout Request');
295+
return;
296+
}
297+
286298
const serviceProvider = new SAMLServiceProvider(service);
287-
await serviceProvider.validateLogoutRequest(req.query.SAMLRequest, async (err, result) => {
299+
await serviceProvider.validateLogoutRequest(envelope, async (err, result) => {
288300
if (err) {
289-
SystemLogger.error({ err });
290-
throw new Meteor.Error('Unable to Validate Logout Request');
301+
errorHandler(err);
291302
}
292303

293304
if (!result?.nameID || !result?.idpSession) {
@@ -350,15 +361,16 @@ export class SAML {
350361
}
351362

352363
private static async processLogoutResponse(req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions): Promise<void> {
353-
if (!req.query.SAMLResponse) {
364+
const envelope = await getSAMLEnvelope(req, 'SAMLResponse', 'HTTP-Redirect');
365+
if (!envelope) {
354366
SAMLUtils.error({ msg: 'Invalid LogoutResponse received: missing SAMLResponse parameter.', query: req.query });
355367
throw new Error('Invalid LogoutResponse received.');
356368
}
357369

358370
const serviceProvider = new SAMLServiceProvider(service);
359-
await serviceProvider.validateLogoutResponse(req.query.SAMLResponse, async (err, inResponseTo) => {
371+
await serviceProvider.validateLogoutResponse(envelope, async (err, inResponseTo) => {
360372
if (err) {
361-
return;
373+
throw new Error('Logout Response Validation failed');
362374
}
363375

364376
if (!inResponseTo) {
@@ -420,15 +432,29 @@ export class SAML {
420432
res.end();
421433
}
422434

423-
private static processValidateAction(
435+
private static async processValidateAction(
424436
req: IIncomingMessage,
425437
res: ServerResponse,
426438
service: IServiceProviderOptions,
427439
_samlObject: ISAMLAction,
428-
): void {
440+
): Promise<void> {
441+
const redirect = (url?: string) => {
442+
res.writeHead(302, {
443+
Location: url ?? Meteor.absoluteUrl(),
444+
});
445+
res.end();
446+
};
447+
448+
const envelope = await getSAMLEnvelope(req, 'SAMLResponse', 'HTTP-POST').catch((err) => SAMLUtils.error(err));
449+
450+
if (!envelope) {
451+
SAMLUtils.error({ msg: 'No envelope found for SAML Response' });
452+
return redirect();
453+
}
454+
429455
const serviceProvider = new SAMLServiceProvider(service);
430-
SAMLUtils.relayState = req.body.RelayState;
431-
serviceProvider.validateResponse(req.body.SAMLResponse, async (err, profile /* , loggedOut*/) => {
456+
SAMLUtils.relayState = envelope.relayState ?? null;
457+
serviceProvider.validateResponse(envelope, async (err, profile /* , loggedOut*/) => {
432458
try {
433459
if (err) {
434460
SAMLUtils.error(err);
@@ -450,16 +476,10 @@ export class SAML {
450476

451477
await this.storeCredential(credentialToken, loginResult);
452478
const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken));
453-
res.writeHead(302, {
454-
Location: url,
455-
});
456-
res.end();
479+
redirect(url);
457480
} catch (err) {
458481
SAMLUtils.error({ err });
459-
res.writeHead(302, {
460-
Location: Meteor.absoluteUrl(),
461-
});
462-
res.end();
482+
redirect();
463483
}
464484
});
465485
}

0 commit comments

Comments
 (0)