diff --git a/packages/twenty-server/src/engine/core-modules/file/file.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/file/file.utils.spec.ts index d6a19d3f9..b5919ac37 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file.utils.spec.ts @@ -1,8 +1,11 @@ +import { BadRequestException } from '@nestjs/common'; + import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; import { checkFilename, checkFilePath, + checkFileFolder, } from 'src/engine/core-modules/file/file.utils'; describe('FileUtils', () => { @@ -58,4 +61,32 @@ describe('FileUtils', () => { expect(() => checkFilename(filename)).toThrow(`Filename is not allowed`); }); }); + + describe('checkFileFolder', () => { + it('should return the root folder when it is allowed', () => { + expect(checkFileFolder(`${FileFolder.Attachment}/file.txt`)).toBe( + FileFolder.Attachment, + ); + }); + + it('should throw BadRequestException for disallowed folders', () => { + expect(() => checkFileFolder('invalid-folder/file.txt')).toThrow( + BadRequestException, + ); + }); + + it('should sanitize null characters in file path', () => { + expect(() => checkFileFolder('\0invalid-folder/file.txt')).toThrow( + BadRequestException, + ); + }); + + it('should handle edge cases like empty file path', () => { + expect(() => checkFileFolder('')).toThrow(BadRequestException); + }); + + it('should handle cases where filePath has no folder', () => { + expect(() => checkFileFolder('file.txt')).toThrow(BadRequestException); + }); + }); }); diff --git a/packages/twenty-server/src/engine/core-modules/file/file.utils.ts b/packages/twenty-server/src/engine/core-modules/file/file.utils.ts index 0d7b042d2..412bcb0c9 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file.utils.ts @@ -48,3 +48,18 @@ export const checkFilename = (filename: string) => { return basename(sanitizedFilename); }; + +export const checkFileFolder = (filePath: string): FileFolder => { + const allowedFolders = Object.values(FileFolder).map((value) => + kebabCase(value), + ); + + const sanitizedFilePath = filePath.replace(/\0/g, ''); + const [rootFolder] = sanitizedFilePath.split('/'); + + if (!allowedFolders.includes(rootFolder as AllowedFolders)) { + throw new BadRequestException(`Folder ${rootFolder} is not allowed`); + } + + return rootFolder as FileFolder; +}; diff --git a/packages/twenty-server/src/engine/core-modules/file/guards/file-path-guard.ts b/packages/twenty-server/src/engine/core-modules/file/guards/file-path-guard.ts index 0cbe988a0..e74374e42 100644 --- a/packages/twenty-server/src/engine/core-modules/file/guards/file-path-guard.ts +++ b/packages/twenty-server/src/engine/core-modules/file/guards/file-path-guard.ts @@ -1,5 +1,8 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { fileFolderConfigs } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; + +import { checkFileFolder } from 'src/engine/core-modules/file/file.utils'; import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; @Injectable() @@ -8,6 +11,10 @@ export class FilePathGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); + const fileFolder = checkFileFolder(request.params[0]); + const ignoreExpirationToken = + fileFolderConfigs[fileFolder].ignoreExpirationToken; + const query = request.query; if (!query || !query['token']) { @@ -18,6 +25,7 @@ export class FilePathGuard implements CanActivate { const payload = await this.jwtWrapperService.verifyWorkspaceToken( query['token'], 'FILE', + ignoreExpirationToken ? { ignoreExpiration: true } : {}, ); if (!payload.workspaceId) { diff --git a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts index 57afbd72c..845783aa5 100644 --- a/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/file/interfaces/file-folder.interface.ts @@ -11,3 +11,25 @@ export enum FileFolder { registerEnumType(FileFolder, { name: 'FileFolder', }); + +export type FileFolderConfig = { + ignoreExpirationToken: boolean; +}; + +export const fileFolderConfigs: Record = { + [FileFolder.ProfilePicture]: { + ignoreExpirationToken: true, + }, + [FileFolder.WorkspaceLogo]: { + ignoreExpirationToken: true, + }, + [FileFolder.Attachment]: { + ignoreExpirationToken: false, + }, + [FileFolder.PersonPicture]: { + ignoreExpirationToken: false, + }, + [FileFolder.ServerlessFunction]: { + ignoreExpirationToken: false, + }, +};