Harden local file storage driver path resolution (#17783)

## Summary

- Normalize all file paths with `path.resolve` instead of `join` to
properly handle `..` segments in file path inputs
- Add `assertPathIsWithinStorage` guard on all write, delete, move,
copy, and existence-check operations
- Introduce `ACCESS_DENIED` exception code with i18n-ready user-friendly
message
- Read path already had realpath-based validation; updated its error
code to `ACCESS_DENIED` for consistency

## Test plan

- [x] Typecheck passes
- [x] Lint passes
- [x] Manual: verify file upload/download still works with valid paths
- [x] Manual: verify `../` in file paths is rejected with ACCESS_DENIED


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <etiennejouan@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2026-02-09 10:54:10 +01:00
committed by GitHub
parent 15f09736b2
commit ece265c6e4
30 changed files with 614 additions and 246 deletions
@@ -40,7 +40,7 @@ export class FileUploadService {
mimeType: string | undefined;
folder: string;
}) {
await this.fileStorage.writeFile({
await this.fileStorage.writeFileLegacy({
file,
name: filename,
mimeType,
@@ -80,7 +80,7 @@ export class FilesFieldService {
},
});
return await this.fileStorageService.writeFile_v2({
return await this.fileStorageService.writeFile({
sourceFile: sanitizedFile,
resourcePath: `${fieldMetadata.universalIdentifier}/${name}`,
mimeType,
@@ -153,7 +153,7 @@ export class FilesFieldService {
},
});
return await this.fileStorageService.readFile_v2({
return await this.fileStorageService.readFile({
resourcePath: removeFileFolderFromFileEntityPath(file.path),
fileFolder: FileFolder.FilesField,
applicationUniversalIdentifier: application.universalIdentifier,
@@ -21,7 +21,7 @@ describe('FileService', () => {
{
provide: FileStorageService,
useValue: {
copy: jest.fn(),
copyLegacy: jest.fn(),
},
},
{
@@ -50,7 +50,7 @@ describe('FileService', () => {
'newWorkspaceId',
);
expect(fileStorageService.copy).toHaveBeenCalledWith({
expect(fileStorageService.copyLegacy).toHaveBeenCalledWith({
from: {
folderPath: 'workspace-workspaceId/path/to',
filename: 'file',
@@ -33,7 +33,7 @@ export class FileService {
): Promise<Readable> {
const workspaceFolderPath = `workspace-${workspaceId}/${folderPath}`;
return await this.fileStorageService.readFile({
return await this.fileStorageService.readFileLegacy({
filePath: `${workspaceFolderPath}/${filename}`,
});
}
@@ -85,7 +85,7 @@ export class FileService {
}) {
const workspaceFolderPath = `workspace-${workspaceId}/${folderPath}`;
return await this.fileStorageService.delete({
return await this.fileStorageService.deleteLegacy({
folderPath: workspaceFolderPath,
filename,
});
@@ -95,7 +95,7 @@ export class FileService {
const workspaceFolderPath = `workspace-${workspaceId}`;
const isWorkspaceFolderFound =
await this.fileStorageService.checkFolderExists({
await this.fileStorageService.checkFolderExistsLegacy({
folderPath: workspaceFolderPath,
});
@@ -103,7 +103,7 @@ export class FileService {
return;
}
return await this.fileStorageService.delete({
return await this.fileStorageService.deleteLegacy({
folderPath: workspaceFolderPath,
});
}
@@ -120,7 +120,7 @@ export class FileService {
const toFilename = uuidV4() + extname(fromFilename);
await this.fileStorageService.copy({
await this.fileStorageService.copyLegacy({
from: {
folderPath: `${fromWorkspaceFolderPath}/${subFolder}`,
filename: fromFilename,