Skip to content

Commit b08592e

Browse files
fix: imported fixes 02-18-2026-002 (#38770)
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com> Co-authored-by: Abhinav Kumar <abhinav@avitechlab.com>
1 parent 0cf41cf commit b08592e

10 files changed

Lines changed: 563 additions & 15 deletions

File tree

apps/meteor/.mocharc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ module.exports = {
3030
'app/file-upload/server/**/*.spec.ts',
3131
'app/statistics/server/**/*.spec.ts',
3232
'app/livechat/server/lib/**/*.spec.ts',
33+
'app/utils/server/**/*.spec.ts',
3334
],
3435
};

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

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,25 @@ const settingsGetMap = new Map();
1111
const messagesModelStub = {
1212
find: sinon.stub(),
1313
};
14+
const usersModelStub = {
15+
findOneByIdAndLoginToken: sinon.stub(),
16+
};
17+
const subscriptionsModelStub = {
18+
findOneByRoomIdAndUserId: sinon.stub(),
19+
};
20+
const validateAndDecodeJWTStub = sinon.stub();
21+
const systemLoggerStub = {
22+
error: sinon.stub(),
23+
};
24+
const roomCoordinatorStub = {
25+
getRoomDirectives: sinon.stub(),
26+
};
1427

1528
const { FileUpload, FileUploadClass } = proxyquire.noCallThru().load('./FileUpload', {
1629
'@rocket.chat/models': {
1730
Messages: messagesModelStub,
31+
Users: usersModelStub,
32+
Subscriptions: subscriptionsModelStub,
1833
},
1934
'meteor/check': sinon.stub(),
2035
'meteor/meteor': sinon.stub(),
@@ -23,15 +38,19 @@ const { FileUpload, FileUploadClass } = proxyquire.noCallThru().load('./FileUplo
2338
'stream-buffers': sinon.stub(),
2439
'@rocket.chat/tools': sinon.stub(),
2540
'../../../../server/lib/i18n': sinon.stub(),
26-
'../../../../server/lib/logger/system': sinon.stub(),
27-
'../../../../server/lib/rooms/roomCoordinator': sinon.stub(),
41+
'../../../../server/lib/logger/system': { SystemLogger: systemLoggerStub },
42+
'../../../../server/lib/rooms/roomCoordinator': { roomCoordinator: roomCoordinatorStub },
2843
'../../../../server/ufs': sinon.stub(),
2944
'../../../../server/ufs/ufs-methods': sinon.stub(),
3045
'../../../settings/server': { settings: settingsStub },
3146
'../../../utils/lib/mimeTypes': sinon.stub(),
32-
'../../../utils/server/lib/JWTHelper': sinon.stub(),
47+
'../../../utils/server/lib/JWTHelper': {
48+
validateAndDecodeJWT: validateAndDecodeJWTStub,
49+
generateJWT: sinon.stub(),
50+
},
3351
'../../../utils/server/restrictions': sinon.stub(),
3452
'../../../api/server/lib/MultipartUploadHandler': sinon.stub(),
53+
'@rocket.chat/account-utils': { hashLoginToken: sinon.stub().callsFake((token) => `hashed_${token}`) },
3554
});
3655

3756
describe('FileUpload', () => {
@@ -45,6 +64,13 @@ describe('FileUpload', () => {
4564
messagesModelStub.find.reset();
4665
fakeStorageModel.findOneById.reset();
4766
fakeStorageModel.deleteFile.reset();
67+
usersModelStub.findOneByIdAndLoginToken.reset();
68+
subscriptionsModelStub.findOneByRoomIdAndUserId.reset();
69+
validateAndDecodeJWTStub.reset();
70+
systemLoggerStub.error.reset();
71+
roomCoordinatorStub.getRoomDirectives.reset();
72+
settingsGetMap.clear();
73+
settingsGetMap.set('FileUpload_Storage_Type', 'fakeStorage');
4874
});
4975

5076
it('should not remove any file if no room id is provided', async () => {
@@ -101,4 +127,165 @@ describe('FileUpload', () => {
101127
expect(fakeStorageModel.deleteFile.calledWith('file-id')).to.be.true;
102128
expect(fakeStorageModel.deleteFile.calledWith('thumbnail-id')).to.be.true;
103129
});
130+
131+
describe('requestCanAccessFiles', () => {
132+
it('should allow access if FileUpload_ProtectFiles is false', async () => {
133+
settingsGetMap.set('FileUpload_ProtectFiles', false);
134+
135+
const request = {
136+
headers: {},
137+
url: '/file-upload/test-file-id/test-file.png',
138+
} as any;
139+
140+
const result = await FileUpload.requestCanAccessFiles(request);
141+
expect(result).to.be.true;
142+
});
143+
144+
it('should allow access if no url is provided', async () => {
145+
settingsGetMap.set('FileUpload_ProtectFiles', true);
146+
147+
const request = {
148+
headers: {},
149+
url: undefined,
150+
} as any;
151+
152+
const result = await FileUpload.requestCanAccessFiles(request);
153+
expect(result).to.be.true;
154+
});
155+
156+
it('should deny access if FileUpload_Enable_json_web_token_for_files is true but no token is provided', async () => {
157+
settingsGetMap.set('FileUpload_ProtectFiles', true);
158+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
159+
160+
const request = {
161+
headers: {},
162+
url: '/file-upload/test-file-id/test-file.png',
163+
} as any;
164+
165+
const file = {
166+
_id: 'test-file-id',
167+
rid: 'test-room-id',
168+
} as any;
169+
170+
const result = await FileUpload.requestCanAccessFiles(request, file);
171+
expect(result).to.be.false;
172+
});
173+
174+
it('should deny access if FileUpload_json_web_token_secret_for_files is not configured', async () => {
175+
settingsGetMap.set('FileUpload_ProtectFiles', true);
176+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
177+
settingsGetMap.set('FileUpload_json_web_token_secret_for_files', '');
178+
179+
const request = {
180+
headers: {},
181+
url: '/file-upload/test-file-id/test-file.png?token=some-token',
182+
} as any;
183+
184+
const file = {
185+
_id: 'test-file-id',
186+
rid: 'test-room-id',
187+
} as any;
188+
189+
const result = await FileUpload.requestCanAccessFiles(request, file);
190+
expect(result).to.be.false;
191+
expect(systemLoggerStub.error.calledOnce).to.be.true;
192+
});
193+
194+
it('should deny access if an invalid token is provided', async () => {
195+
settingsGetMap.set('FileUpload_ProtectFiles', true);
196+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
197+
settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret');
198+
validateAndDecodeJWTStub.returns(null);
199+
200+
const request = {
201+
headers: {},
202+
url: '/file-upload/test-file-id/test-file.png?token=invalid-token',
203+
} as any;
204+
205+
const file = {
206+
_id: 'test-file-id',
207+
rid: 'test-room-id',
208+
} as any;
209+
210+
const result = await FileUpload.requestCanAccessFiles(request, file);
211+
expect(result).to.be.false;
212+
expect(validateAndDecodeJWTStub.calledOnce).to.be.true;
213+
});
214+
215+
it('should deny access if token is invalid or payload cannot be decoded', async () => {
216+
settingsGetMap.set('FileUpload_ProtectFiles', true);
217+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
218+
settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret');
219+
validateAndDecodeJWTStub.returns(null);
220+
221+
const request = {
222+
headers: {},
223+
url: '/file-upload/test-file-id/test-file.png?token=valid-token',
224+
} as any;
225+
226+
const file = {
227+
_id: 'test-file-id',
228+
rid: 'test-room-id',
229+
} as any;
230+
231+
const result = await FileUpload.requestCanAccessFiles(request, file);
232+
expect(result).to.be.false;
233+
});
234+
235+
it('should deny access if the fileId and rid in the token do not match the requested file', async () => {
236+
settingsGetMap.set('FileUpload_ProtectFiles', true);
237+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
238+
settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret');
239+
validateAndDecodeJWTStub.returns({ fileId: 'different-file-id', rid: 'different-room-id', userId: 'test-user-id' });
240+
241+
const request = {
242+
headers: {},
243+
url: '/file-upload/test-file-id/test-file.png?token=valid-token',
244+
} as any;
245+
246+
const file = {
247+
_id: 'test-file-id',
248+
rid: 'test-room-id',
249+
} as any;
250+
251+
const result = await FileUpload.requestCanAccessFiles(request, file);
252+
expect(result).to.be.false;
253+
});
254+
255+
it('should deny access if file object is not provided when using JWT', async () => {
256+
settingsGetMap.set('FileUpload_ProtectFiles', true);
257+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
258+
settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret');
259+
validateAndDecodeJWTStub.returns({ fileId: 'test-file-id', rid: 'test-room-id', userId: 'test-user-id' });
260+
261+
const request = {
262+
headers: {},
263+
url: '/file-upload/test-file-id/test-file.png?token=valid-token',
264+
} as any;
265+
266+
const result = await FileUpload.requestCanAccessFiles(request, undefined);
267+
expect(result).to.be.false;
268+
});
269+
270+
it('should allow access when everything is valid: token is valid, secret configured, and file/room match', async () => {
271+
settingsGetMap.set('FileUpload_ProtectFiles', true);
272+
settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true);
273+
settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret');
274+
validateAndDecodeJWTStub.returns({ fileId: 'test-file-id', rid: 'test-room-id', userId: 'test-user-id' });
275+
276+
const request = {
277+
headers: {},
278+
url: '/file-upload/test-file-id/test-file.png?token=valid-token',
279+
} as any;
280+
281+
const file = {
282+
_id: 'test-file-id',
283+
rid: 'test-room-id',
284+
} as any;
285+
286+
const result = await FileUpload.requestCanAccessFiles(request, file);
287+
expect(result).to.be.true;
288+
expect(validateAndDecodeJWTStub.calledOnceWith('valid-token', 'test-secret')).to.be.true;
289+
});
290+
});
104291
});

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { MultipartUploadHandler } from '../../../api/server/lib/MultipartUploadH
3535
import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
3636
import { settings } from '../../../settings/server';
3737
import { mime } from '../../../utils/lib/mimeTypes';
38-
import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper';
38+
import { validateAndDecodeJWT, generateJWT } from '../../../utils/server/lib/JWTHelper';
3939
import { fileUploadIsValidContentType } from '../../../utils/server/restrictions';
4040

4141
const cookie = new Cookies();
@@ -471,12 +471,36 @@ export const FileUpload = {
471471
.getRoomDirectives(rc_room_type)
472472
.canAccessUploadedFile({ rc_uid: rc_uid || '', rc_rid: rc_rid || '', rc_token: rc_token || '' });
473473

474-
const isAuthorizedByJWT = () =>
475-
settings.get('FileUpload_Enable_json_web_token_for_files') &&
476-
token &&
477-
isValidJWT(token as string, settings.get('FileUpload_json_web_token_secret_for_files'));
474+
const isAuthorizedByJWT: () => boolean = () => {
475+
if (!token || typeof token !== 'string' || !settings.get('FileUpload_Enable_json_web_token_for_files')) {
476+
return false;
477+
}
478+
479+
if (!settings.get('FileUpload_json_web_token_secret_for_files')) {
480+
SystemLogger.error('FileUpload_json_web_token_secret_for_files is not configured. Cannot validate JWT for file access.');
481+
return false;
482+
}
483+
484+
const payload = validateAndDecodeJWT(token, settings.get('FileUpload_json_web_token_secret_for_files'));
485+
486+
if (!payload) {
487+
return false;
488+
}
489+
490+
const { fileId, rid } = payload as { fileId: string; rid: string };
491+
492+
if (!fileId || !rid) {
493+
return false;
494+
}
495+
496+
if (fileId !== file?._id || rid !== file?.rid) {
497+
return false;
498+
}
499+
500+
return true;
501+
};
478502

479-
if ((await isAuthorizedByRoom()) || isAuthorizedByJWT()) {
503+
if (isAuthorizedByJWT() || (await isAuthorizedByRoom())) {
480504
return true;
481505
}
482506

@@ -659,7 +683,11 @@ export const FileUpload = {
659683
},
660684

661685
generateJWTToFileUrls({ rid, userId, fileId }: { rid: string; userId: string; fileId: string }) {
662-
if (!settings.get('FileUpload_ProtectFiles') || !settings.get('FileUpload_Enable_json_web_token_for_files')) {
686+
if (
687+
!settings.get('FileUpload_ProtectFiles') ||
688+
!settings.get('FileUpload_Enable_json_web_token_for_files') ||
689+
!settings.get('FileUpload_json_web_token_secret_for_files')
690+
) {
663691
return;
664692
}
665693
return generateJWT(

0 commit comments

Comments
 (0)