FileStorageService Dedicated file and folder code flow + integrity check (#20831)
# Introduction
Next handling mimetype integrity check and checksum integrity check for
s3 storage type
Always expecting a trailing end slash when deleting a folder etc
## Application
Uninstalling an application now deletes all its related files
## File storage service
Making a distincton between folder path and file path
## Validation Pipeline
Every file operation in `FileStorageService.buildOnStoragePath` runs
through `validateResourcePath`, which chains three validators in order:
**1. `validateSafeRelativePath`** -- rejects path traversal attacks
| Input | Result | Error |
|---|---|---|
| `../../../etc/passwd` | Rejected | `Resource path must not contain
path traversal (..)` |
| `/etc/passwd` | Rejected | `Resource path must be relative, not
absolute` |
| `file\0.txt` | Rejected | `Resource path contains null bytes` |
| `..\\..\\etc\\passwd` | Rejected | `Resource path must not contain
backslashes` |
| _(empty)_ | Rejected | `Resource path must not be empty` |
**2. `validateFilenameIntegrity`** -- enforces safe characters, length
limits, extension required
| Input | Result | Error |
|---|---|---|
| `my folder/file.mjs` | Rejected | `A path segment contains invalid
characters...` |
| `Makefile` | Rejected | `Filename must have an extension` |
| `aaa...(256 chars).mjs` | Rejected | `A path segment exceeds the
maximum length of 255 characters` |
| `a/b/.../file.mjs` (1025+ chars) | Rejected | `Resource path exceeds
maximum length of 1024 characters` |
| `src/handlers/index.mjs` | Accepted | -- |
| `my-app/my_file.tsx` | Accepted | -- |
| `v1.0/module.config.mjs` | Accepted | -- |
Allowed characters per segment: `a-z`, `A-Z`, `0-9`, `.`, `-`, `_`
**3. `validateResourceExtension`** -- checks extension against the
`FileFolder` allowlist
| Input | FileFolder | Result | Error |
|---|---|---|---|
| `handler.js` | `BuiltLogicFunction` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `card.tsx` | `BuiltFrontComponent` | Rejected | `Invalid file
extension. Allowed extensions: .mjs` |
| `script.js` | `PublicAsset` | Rejected | `Invalid file extension.
Allowed extensions: .png, .jpg, ...` |
| `index.mjs` | `BuiltLogicFunction` | Accepted | -- |
| `app.tsx` | `Source` | Accepted | -- |
| `photo.png` | `CorePicture` | Accepted | -- (unconfigured folder,
passes through) |
## Consumers
- **`FileStorageService`** -- calls `validateResourcePath`, throws
`FileStorageException` on failure (last-resort defense)
- **Resolver (`uploadApplicationFile`)** -- calls
`validateResourcePath`, throws `ApplicationException` on failure
(user-facing)
- **Flat validators** -- call `validateResourcePath`, push the error to
`validationResult.errors` (non-throwing, collects all errors)
All error messages are translated via Lingui `t` and returned in a
discriminated union `{ isValid: true } | { isValid: false, error: string
}`, letting each consumer decide how to handle failures.
This commit is contained in:
+50
-33
@@ -3,9 +3,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isSafeRelativePath } from 'src/engine/core-modules/file-storage/utils/is-safe-relative-path.util';
|
||||
import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { FrontComponentExceptionCode } from 'src/engine/metadata-modules/front-component/front-component.exception';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
@@ -37,26 +38,34 @@ export class FlatFrontComponentValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatFrontComponent.builtComponentPath) &&
|
||||
!isSafeRelativePath(flatFrontComponent.builtComponentPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: t`Built component path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Built component path contains unsafe characters`,
|
||||
if (isDefined(flatFrontComponent.builtComponentPath)) {
|
||||
const builtPathResult = validateFilePath({
|
||||
resourcePath: flatFrontComponent.builtComponentPath,
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
});
|
||||
|
||||
if (!builtPathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: builtPathResult.error,
|
||||
userFriendlyMessage: msg`Built component path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatFrontComponent.sourceComponentPath) &&
|
||||
!isSafeRelativePath(flatFrontComponent.sourceComponentPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: t`Source component path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Source component path contains unsafe characters`,
|
||||
if (isDefined(flatFrontComponent.sourceComponentPath)) {
|
||||
const sourcePathResult = validateFilePath({
|
||||
resourcePath: flatFrontComponent.sourceComponentPath,
|
||||
fileFolder: FileFolder.Source,
|
||||
});
|
||||
|
||||
if (!sourcePathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: sourcePathResult.error,
|
||||
userFriendlyMessage: msg`Source component path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
@@ -129,26 +138,34 @@ export class FlatFrontComponentValidatorService {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatEntityUpdate.builtComponentPath) &&
|
||||
!isSafeRelativePath(flatEntityUpdate.builtComponentPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: t`Built component path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Built component path contains unsafe characters`,
|
||||
if (isDefined(flatEntityUpdate.builtComponentPath)) {
|
||||
const builtPathResult = validateFilePath({
|
||||
resourcePath: flatEntityUpdate.builtComponentPath,
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
});
|
||||
|
||||
if (!builtPathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: builtPathResult.error,
|
||||
userFriendlyMessage: msg`Built component path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatEntityUpdate.sourceComponentPath) &&
|
||||
!isSafeRelativePath(flatEntityUpdate.sourceComponentPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: t`Source component path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Source component path contains unsafe characters`,
|
||||
if (isDefined(flatEntityUpdate.sourceComponentPath)) {
|
||||
const sourcePathResult = validateFilePath({
|
||||
resourcePath: flatEntityUpdate.sourceComponentPath,
|
||||
fileFolder: FileFolder.Source,
|
||||
});
|
||||
|
||||
if (!sourcePathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: FrontComponentExceptionCode.INVALID_FRONT_COMPONENT_INPUT,
|
||||
message: sourcePathResult.error,
|
||||
userFriendlyMessage: msg`Source component path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
|
||||
+50
-33
@@ -2,9 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isSafeRelativePath } from 'src/engine/core-modules/file-storage/utils/is-safe-relative-path.util';
|
||||
import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { LogicFunctionExceptionCode } from 'src/engine/metadata-modules/logic-function/logic-function.exception';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
@@ -48,26 +49,34 @@ export class FlatLogicFunctionValidatorService {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatEntityUpdate.builtHandlerPath) &&
|
||||
!isSafeRelativePath(flatEntityUpdate.builtHandlerPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: t`Built handler path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Built handler path contains unsafe characters`,
|
||||
if (isDefined(flatEntityUpdate.builtHandlerPath)) {
|
||||
const builtPathResult = validateFilePath({
|
||||
resourcePath: flatEntityUpdate.builtHandlerPath,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
if (!builtPathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: builtPathResult.error,
|
||||
userFriendlyMessage: msg`Built handler path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatEntityUpdate.sourceHandlerPath) &&
|
||||
!isSafeRelativePath(flatEntityUpdate.sourceHandlerPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: t`Source handler path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Source handler path contains unsafe characters`,
|
||||
if (isDefined(flatEntityUpdate.sourceHandlerPath)) {
|
||||
const sourcePathResult = validateFilePath({
|
||||
resourcePath: flatEntityUpdate.sourceHandlerPath,
|
||||
fileFolder: FileFolder.Source,
|
||||
});
|
||||
|
||||
if (!sourcePathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: sourcePathResult.error,
|
||||
userFriendlyMessage: msg`Source handler path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
@@ -136,26 +145,34 @@ export class FlatLogicFunctionValidatorService {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatLogicFunctionToValidate.builtHandlerPath) &&
|
||||
!isSafeRelativePath(flatLogicFunctionToValidate.builtHandlerPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: t`Built handler path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Built handler path contains unsafe characters`,
|
||||
if (isDefined(flatLogicFunctionToValidate.builtHandlerPath)) {
|
||||
const builtPathResult = validateFilePath({
|
||||
resourcePath: flatLogicFunctionToValidate.builtHandlerPath,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
if (!builtPathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: builtPathResult.error,
|
||||
userFriendlyMessage: msg`Built handler path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(flatLogicFunctionToValidate.sourceHandlerPath) &&
|
||||
!isSafeRelativePath(flatLogicFunctionToValidate.sourceHandlerPath)
|
||||
) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: t`Source handler path contains unsafe characters`,
|
||||
userFriendlyMessage: msg`Source handler path contains unsafe characters`,
|
||||
if (isDefined(flatLogicFunctionToValidate.sourceHandlerPath)) {
|
||||
const sourcePathResult = validateFilePath({
|
||||
resourcePath: flatLogicFunctionToValidate.sourceHandlerPath,
|
||||
fileFolder: FileFolder.Source,
|
||||
});
|
||||
|
||||
if (!sourcePathResult.isValid) {
|
||||
validationResult.errors.push({
|
||||
code: LogicFunctionExceptionCode.INVALID_LOGIC_FUNCTION_INPUT,
|
||||
message: sourcePathResult.error,
|
||||
userFriendlyMessage: msg`Source handler path is invalid`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
|
||||
+3
-5
@@ -60,16 +60,14 @@ export class DeleteLogicFunctionActionHandlerService extends WorkspaceMigrationR
|
||||
|
||||
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
|
||||
|
||||
await this.fileStorageService.delete({
|
||||
await this.fileStorageService.deleteFolder({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Source,
|
||||
resourcePath: getLogicFunctionSubfolderForFromSource(
|
||||
flatLogicFunction.id,
|
||||
),
|
||||
folderPath: getLogicFunctionSubfolderForFromSource(flatLogicFunction.id),
|
||||
});
|
||||
|
||||
await this.fileStorageService.delete({
|
||||
await this.fileStorageService.deleteFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ export class UpdateLogicFunctionActionHandlerService extends WorkspaceMigrationR
|
||||
);
|
||||
|
||||
if (builtPathChanged) {
|
||||
await this.fileStorageService.delete({
|
||||
await this.fileStorageService.deleteFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
|
||||
Reference in New Issue
Block a user