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:
@@ -80,8 +80,8 @@ export async function teardown() {
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
throw new Error(
|
||||
`App uninstall failed during teardown: ${JSON.stringify(uninstallResult.error, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -20,6 +20,7 @@ import { DevelopmentApplicationDTO } from 'src/engine/core-modules/application/a
|
||||
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/application-development/dtos/generate-application-token.input';
|
||||
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/application-development/dtos/upload-application-file.input';
|
||||
import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/application-development/dtos/workspace-migration.dto';
|
||||
import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { resolveManifestAssetUrls } from 'src/engine/core-modules/application/application-marketplace/utils/resolve-manifest-asset-urls.util';
|
||||
@@ -219,6 +220,18 @@ export class ApplicationDevelopmentResolver {
|
||||
);
|
||||
}
|
||||
|
||||
const pathValidationResult = validateFilePath({
|
||||
resourcePath: filePath,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
if (!pathValidationResult.isValid) {
|
||||
throw new ApplicationException(
|
||||
pathValidationResult.error,
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const application = await this.applicationService.findByUniversalIdentifier(
|
||||
{
|
||||
universalIdentifier: applicationUniversalIdentifier,
|
||||
|
||||
+347
-186
@@ -2,6 +2,10 @@ import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import {
|
||||
type EachTestingContext,
|
||||
eachTestingContextFilter,
|
||||
} from 'twenty-shared/testing';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
|
||||
@@ -190,141 +194,203 @@ describe('FileStorageService', () => {
|
||||
resourcePath: 'src/components/my-component.mjs',
|
||||
};
|
||||
|
||||
const validFolderIdentifier = {
|
||||
workspaceId: 'workspace-123',
|
||||
applicationUniversalIdentifier: 'app-456',
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
};
|
||||
|
||||
const expectedValidPath =
|
||||
'workspace-123/app-456/built-front-component/src/components/my-component.mjs';
|
||||
|
||||
type RejectedFilePathContext = {
|
||||
resourcePath: string;
|
||||
};
|
||||
|
||||
const REJECTED_FILE_PATHS: EachTestingContext<RejectedFilePathContext>[] = [
|
||||
{
|
||||
title: 'path traversal with ../',
|
||||
context: {
|
||||
resourcePath:
|
||||
'../../../victim-ws/victim-app/built-front-component/secret.mjs',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'absolute path',
|
||||
context: { resourcePath: '/etc/passwd' },
|
||||
},
|
||||
{
|
||||
title: 'single-level traversal escaping fileFolder',
|
||||
context: { resourcePath: '../source/handler.ts' },
|
||||
},
|
||||
{
|
||||
title: 'excess .. segments',
|
||||
context: { resourcePath: 'foo/../../../../../../etc/passwd' },
|
||||
},
|
||||
{
|
||||
title: 'exact 3-level traversal to another tenant',
|
||||
context: {
|
||||
resourcePath:
|
||||
'../../../target-ws/target-app/built-front-component/file.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'empty resource path',
|
||||
context: { resourcePath: '' },
|
||||
},
|
||||
];
|
||||
|
||||
type AcceptedFilePathContext = {
|
||||
resourcePath: string;
|
||||
expectedStoragePath: string;
|
||||
};
|
||||
|
||||
const ACCEPTED_FILE_PATHS: EachTestingContext<AcceptedFilePathContext>[] = [
|
||||
{
|
||||
title: 'valid relative path',
|
||||
context: {
|
||||
resourcePath: 'src/components/my-component.mjs',
|
||||
expectedStoragePath: expectedValidPath,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'deeply nested valid path',
|
||||
context: {
|
||||
resourcePath: 'a/b/c/d/e/f/deep-file.mjs',
|
||||
expectedStoragePath:
|
||||
'workspace-123/app-456/built-front-component/a/b/c/d/e/f/deep-file.mjs',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'path with dots that are not traversal',
|
||||
context: {
|
||||
resourcePath: 'v1.0.0/file.name.mjs',
|
||||
expectedStoragePath:
|
||||
'workspace-123/app-456/built-front-component/v1.0.0/file.name.mjs',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'path with hidden directory (dot-prefixed segment)',
|
||||
context: {
|
||||
resourcePath: '.hidden/file.name.mjs',
|
||||
expectedStoragePath:
|
||||
'workspace-123/app-456/built-front-component/.hidden/file.name.mjs',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('readFile', () => {
|
||||
it('should allow valid relative paths', async () => {
|
||||
await service.readFile(validResourceIdentifier);
|
||||
it.each(eachTestingContextFilter(REJECTED_FILE_PATHS))(
|
||||
'should reject $title',
|
||||
({ context }) => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: context.resourcePath,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.readFile).toHaveBeenCalledWith({
|
||||
filePath: expectedValidPath,
|
||||
});
|
||||
});
|
||||
expect(mockDriver.readFile).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('should reject path traversal with ../', () => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
it.each(eachTestingContextFilter(ACCEPTED_FILE_PATHS))(
|
||||
'should accept $title',
|
||||
async ({ context }) => {
|
||||
await service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath:
|
||||
'../../../victim-ws/victim-app/built-front-component/secret.mjs',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
resourcePath: context.resourcePath,
|
||||
});
|
||||
|
||||
expect(mockDriver.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject absolute paths', () => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '/etc/passwd',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject single-level traversal escaping fileFolder', () => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '../source/handler.ts',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockDriver.readFile).toHaveBeenCalledWith({
|
||||
filePath: context.expectedStoragePath,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('checkFileExists', () => {
|
||||
it('should allow valid relative paths', async () => {
|
||||
await service.checkFileExists(validResourceIdentifier);
|
||||
it.each(eachTestingContextFilter(REJECTED_FILE_PATHS))(
|
||||
'should reject $title',
|
||||
({ context }) => {
|
||||
expect(() =>
|
||||
service.checkFileExists({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: context.resourcePath,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.checkFileExists).toHaveBeenCalledWith({
|
||||
filePath: expectedValidPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject path traversal with ../', () => {
|
||||
expect(() =>
|
||||
service.checkFileExists({
|
||||
...validResourceIdentifier,
|
||||
resourcePath:
|
||||
'../../../other-ws/other-app/built-front-component/file.js',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.checkFileExists).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockDriver.checkFileExists).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getPresignedUrl', () => {
|
||||
it('should reject path traversal', async () => {
|
||||
await expect(
|
||||
service.getPresignedUrl({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '../../../other-ws/file.js',
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
});
|
||||
it.each(eachTestingContextFilter(REJECTED_FILE_PATHS))(
|
||||
'should reject $title',
|
||||
async ({ context }) => {
|
||||
await expect(
|
||||
service.getPresignedUrl({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: context.resourcePath,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
});
|
||||
|
||||
expect(mockDriver.getPresignedUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockDriver.getPresignedUrl).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('downloadFile', () => {
|
||||
it('should reject path traversal', () => {
|
||||
expect(() =>
|
||||
service.downloadFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '../../../other-ws/file.js',
|
||||
localPath: '/tmp/download.js',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
it.each(eachTestingContextFilter(REJECTED_FILE_PATHS))(
|
||||
'should reject $title',
|
||||
({ context }) => {
|
||||
expect(() =>
|
||||
service.downloadFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: context.resourcePath,
|
||||
localPath: '/tmp/download.js',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.downloadFile).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockDriver.downloadFile).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('writeFile', () => {
|
||||
it('should reject path traversal on write', async () => {
|
||||
await expect(
|
||||
service.writeFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath:
|
||||
'../../../victim-ws/victim-app/built-front-component/overwrite.mjs',
|
||||
sourceFile: Buffer.from('malicious'),
|
||||
mimeType: 'application/javascript',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
});
|
||||
it.each(eachTestingContextFilter(REJECTED_FILE_PATHS))(
|
||||
'should reject $title',
|
||||
async ({ context }) => {
|
||||
await expect(
|
||||
service.writeFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: context.resourcePath,
|
||||
sourceFile: Buffer.from('malicious'),
|
||||
mimeType: 'application/javascript',
|
||||
settings: { isTemporaryFile: false, toDelete: false },
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
});
|
||||
|
||||
expect(mockDriver.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(mockDriver.writeFile).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('should allow valid writes', async () => {
|
||||
await service.writeFile({
|
||||
@@ -342,18 +408,178 @@ describe('FileStorageService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should reject path traversal on delete', async () => {
|
||||
await expect(
|
||||
service.delete({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '../../../other-ws/other-app/folder',
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
describe('deleteFile', () => {
|
||||
it.each(eachTestingContextFilter(REJECTED_FILE_PATHS))(
|
||||
'should reject $title',
|
||||
async ({ context }) => {
|
||||
await expect(
|
||||
service.deleteFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: context.resourcePath,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
});
|
||||
|
||||
expect(mockDriver.delete).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('should call driver.delete with dirname and basename', async () => {
|
||||
await service.deleteFile(validResourceIdentifier);
|
||||
|
||||
expect(mockDriver.delete).toHaveBeenCalledWith({
|
||||
folderPath:
|
||||
'workspace-123/app-456/built-front-component/src/components',
|
||||
filename: 'my-component.mjs',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFolder', () => {
|
||||
type RejectedFolderPathContext = {
|
||||
folderPath: string;
|
||||
};
|
||||
|
||||
const REJECTED_FOLDER_PATHS: EachTestingContext<RejectedFolderPathContext>[] =
|
||||
[
|
||||
{
|
||||
title: 'path traversal with ../',
|
||||
context: { folderPath: '../../../other-ws/other-app/folder' },
|
||||
},
|
||||
{
|
||||
title: 'absolute path',
|
||||
context: { folderPath: '/etc/secret-folder' },
|
||||
},
|
||||
{
|
||||
title: 'empty folder path',
|
||||
context: { folderPath: '' },
|
||||
},
|
||||
{
|
||||
title: 'folder path with special characters (spaces)',
|
||||
context: { folderPath: 'my folder/data' },
|
||||
},
|
||||
{
|
||||
title: 'folder path with shell metacharacters',
|
||||
context: { folderPath: 'folder;rm -rf/' },
|
||||
},
|
||||
{
|
||||
title: 'file path with .mjs extension',
|
||||
context: { folderPath: 'src/logic-functions/handler.mjs' },
|
||||
},
|
||||
{
|
||||
title: 'file path with .ts extension',
|
||||
context: { folderPath: 'src/components/index.ts' },
|
||||
},
|
||||
{
|
||||
title: 'file path with .json extension',
|
||||
context: { folderPath: 'config/settings.json' },
|
||||
},
|
||||
{
|
||||
title: 'dotted version string (extname detects .0)',
|
||||
context: { folderPath: 'v1.0.0' },
|
||||
},
|
||||
{
|
||||
title: 'numeric-only extension (.7z)',
|
||||
context: { folderPath: 'archive.7z' },
|
||||
},
|
||||
];
|
||||
|
||||
type AcceptedFolderPathContext = {
|
||||
folderPath: string;
|
||||
expectedStoragePath: string;
|
||||
};
|
||||
|
||||
const ACCEPTED_FOLDER_PATHS: EachTestingContext<AcceptedFolderPathContext>[] =
|
||||
[
|
||||
{
|
||||
title: 'UUID folder path',
|
||||
context: {
|
||||
folderPath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
expectedStoragePath:
|
||||
'workspace-123/app-456/built-front-component/8b2df3cc-23ad-4e1b-87fd-f880d4cefd58/',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'simple folder name',
|
||||
context: {
|
||||
folderPath: 'my-folder',
|
||||
expectedStoragePath:
|
||||
'workspace-123/app-456/built-front-component/my-folder/',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'nested folder path with numeric ranges',
|
||||
context: {
|
||||
folderPath: '0000-0999/tmp200/toto',
|
||||
expectedStoragePath:
|
||||
'workspace-123/app-456/built-front-component/0000-0999/tmp200/toto/',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(eachTestingContextFilter(REJECTED_FOLDER_PATHS))(
|
||||
'should reject $title',
|
||||
async ({ context }) => {
|
||||
await expect(
|
||||
service.deleteFolder({
|
||||
...validFolderIdentifier,
|
||||
folderPath: context.folderPath,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
});
|
||||
|
||||
expect(mockDriver.delete).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(eachTestingContextFilter(ACCEPTED_FOLDER_PATHS))(
|
||||
'should accept $title',
|
||||
async ({ context }) => {
|
||||
await service.deleteFolder({
|
||||
...validFolderIdentifier,
|
||||
folderPath: context.folderPath,
|
||||
});
|
||||
|
||||
expect(mockDriver.delete).toHaveBeenCalledWith({
|
||||
folderPath: context.expectedStoragePath,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('deleteByFileId', () => {
|
||||
it('should delegate to deleteFile with the correct resource path', async () => {
|
||||
mockFileRepository.findOneOrFail.mockResolvedValue({
|
||||
id: 'file-id',
|
||||
path: 'built-front-component/src/components/my-component.mjs',
|
||||
applicationId: 'app-id',
|
||||
workspaceId: 'workspace-123',
|
||||
});
|
||||
|
||||
expect(mockDriver.delete).not.toHaveBeenCalled();
|
||||
mockApplicationRepository.findOneOrFail.mockResolvedValue({
|
||||
id: 'app-id',
|
||||
universalIdentifier: 'app-456',
|
||||
});
|
||||
|
||||
await service.deleteByFileId({
|
||||
fileId: 'file-id',
|
||||
workspaceId: 'workspace-123',
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
});
|
||||
|
||||
expect(mockDriver.delete).toHaveBeenCalledWith({
|
||||
folderPath:
|
||||
'workspace-123/app-456/built-front-component/src/components',
|
||||
filename: 'my-component.mjs',
|
||||
});
|
||||
|
||||
expect(mockFileRepository.delete).toHaveBeenCalledWith({
|
||||
path: 'built-front-component/src/components/my-component.mjs',
|
||||
applicationId: 'app-id',
|
||||
workspaceId: 'workspace-123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -390,70 +616,5 @@ describe('FileStorageService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should reject traversal with excess .. segments', () => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: 'foo/../../../../../../etc/passwd',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept deeply nested valid paths', async () => {
|
||||
await service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: 'a/b/c/d/e/f/deep-file.mjs',
|
||||
});
|
||||
|
||||
expect(mockDriver.readFile).toHaveBeenCalledWith({
|
||||
filePath:
|
||||
'workspace-123/app-456/built-front-component/a/b/c/d/e/f/deep-file.mjs',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject exact 3-level traversal to another tenant', () => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath:
|
||||
'../../../target-ws/target-app/built-front-component/file.js',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept paths with dots that are not traversal', async () => {
|
||||
await service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '.hidden/file.name.ext',
|
||||
});
|
||||
|
||||
expect(mockDriver.readFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject empty resource path', () => {
|
||||
expect(() =>
|
||||
service.readFile({
|
||||
...validResourceIdentifier,
|
||||
resourcePath: '',
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockDriver.readFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export const ALLOWED_EXTENSIONS_BY_APPLICATION_FILE_FOLDER = {
|
||||
[FileFolder.BuiltLogicFunction]: { '.mjs': true },
|
||||
[FileFolder.BuiltFrontComponent]: { '.mjs': true },
|
||||
[FileFolder.Source]: { '.ts': true, '.tsx': true, '.json': true },
|
||||
[FileFolder.Dependencies]: { '.json': true, '.lock': true },
|
||||
} as const satisfies Partial<Record<FileFolder, Record<string, true>>>;
|
||||
+146
-45
@@ -9,10 +9,16 @@ import { Like, Repository, type QueryRunner } from 'typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
|
||||
import { assertStoragePathIsWithinWorkspace } from 'src/engine/core-modules/file-storage/utils/assert-storage-path-is-within-workspace.util';
|
||||
import { assertResourcePathIsSafe } from 'src/engine/core-modules/file-storage/utils/assert-resource-path-is-safe.util';
|
||||
import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util';
|
||||
import { validateFolderPath } from 'src/engine/core-modules/file-storage/utils/validate-folder-path.util';
|
||||
import { validateStoragePathIsWithinWorkspaceOrThrow } from 'src/engine/core-modules/file-storage/utils/validate-storage-path-is-within-workspace-or-throw.util';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileSettings } from 'src/engine/core-modules/file/types/file-settings.types';
|
||||
import { removeFileFolderFromFileEntityPath } from 'src/engine/core-modules/file/utils/remove-file-folder-from-file-entity-path.utils';
|
||||
|
||||
export type ResourceIdentifier = {
|
||||
workspaceId: string;
|
||||
@@ -31,29 +37,84 @@ export class FileStorageService {
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
) {}
|
||||
|
||||
private buildOnStoragePath({
|
||||
private buildStoragePathWithinWorkspaceOrThrow({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
resourcePath,
|
||||
}: ResourceIdentifier): string {
|
||||
assertResourcePathIsSafe(resourcePath);
|
||||
relativePath,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
fileFolder: FileFolder;
|
||||
relativePath: string;
|
||||
}): { onStoragePath: string; resourcePath: string } {
|
||||
const resourcePath = join(fileFolder, relativePath).replace(/\/+/g, '/');
|
||||
|
||||
const onStoragePath = join(
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
resourcePath,
|
||||
).replace(/\/+/g, '/');
|
||||
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
validateStoragePathIsWithinWorkspaceOrThrow({
|
||||
onStoragePath,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
});
|
||||
|
||||
return onStoragePath;
|
||||
return { onStoragePath, resourcePath };
|
||||
}
|
||||
|
||||
private validateAndBuildFileStoragePath(params: ResourceIdentifier): {
|
||||
onStorageFilePath: string;
|
||||
filePath: string;
|
||||
} {
|
||||
const validationResult = validateFilePath({
|
||||
resourcePath: params.resourcePath,
|
||||
fileFolder: params.fileFolder,
|
||||
});
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
throw new FileStorageException(
|
||||
validationResult.error,
|
||||
FileStorageExceptionCode.ACCESS_DENIED,
|
||||
);
|
||||
}
|
||||
|
||||
const { onStoragePath, resourcePath } =
|
||||
this.buildStoragePathWithinWorkspaceOrThrow({
|
||||
...params,
|
||||
relativePath: params.resourcePath,
|
||||
});
|
||||
|
||||
return { onStorageFilePath: onStoragePath, filePath: resourcePath };
|
||||
}
|
||||
|
||||
private validateAndBuildFolderStoragePath(
|
||||
params: Omit<ResourceIdentifier, 'resourcePath'> & { folderPath: string },
|
||||
): { onStorageFolderPath: string; folderPath: string } {
|
||||
const validationResult = validateFolderPath({
|
||||
folderPath: params.folderPath,
|
||||
});
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
throw new FileStorageException(
|
||||
validationResult.error,
|
||||
FileStorageExceptionCode.ACCESS_DENIED,
|
||||
);
|
||||
}
|
||||
|
||||
const { onStoragePath, resourcePath } =
|
||||
this.buildStoragePathWithinWorkspaceOrThrow({
|
||||
...params,
|
||||
relativePath: params.folderPath,
|
||||
});
|
||||
|
||||
return {
|
||||
onStorageFolderPath: `${onStoragePath}/`,
|
||||
folderPath: `${resourcePath}/`,
|
||||
};
|
||||
}
|
||||
|
||||
async writeFile({
|
||||
@@ -89,22 +150,23 @@ export class FileStorageService {
|
||||
},
|
||||
});
|
||||
|
||||
const onStoragePath = this.buildOnStoragePath({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
resourcePath,
|
||||
});
|
||||
const { onStorageFilePath, filePath } =
|
||||
this.validateAndBuildFileStoragePath({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
resourcePath,
|
||||
});
|
||||
|
||||
await driver.writeFile({
|
||||
filePath: onStoragePath,
|
||||
filePath: onStorageFilePath,
|
||||
mimeType,
|
||||
sourceFile,
|
||||
});
|
||||
|
||||
await fileRepository.upsert(
|
||||
{
|
||||
path: `${fileFolder}/${resourcePath}`,
|
||||
path: filePath,
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
id: fileId,
|
||||
@@ -120,7 +182,7 @@ export class FileStorageService {
|
||||
|
||||
return await fileRepository.findOneOrFail({
|
||||
where: {
|
||||
path: `${fileFolder}/${resourcePath}`,
|
||||
path: filePath,
|
||||
applicationId: application.id,
|
||||
workspaceId,
|
||||
},
|
||||
@@ -135,10 +197,10 @@ export class FileStorageService {
|
||||
},
|
||||
): Promise<string | null> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
const onStoragePath = this.buildOnStoragePath(params);
|
||||
const { onStorageFilePath } = this.validateAndBuildFileStoragePath(params);
|
||||
|
||||
return driver.getPresignedUrl({
|
||||
filePath: onStoragePath,
|
||||
filePath: onStorageFilePath,
|
||||
expiresInSeconds: params.expiresInSeconds,
|
||||
responseContentType: params.responseContentType,
|
||||
responseContentDisposition: params.responseContentDisposition,
|
||||
@@ -148,19 +210,19 @@ export class FileStorageService {
|
||||
readFile(params: ResourceIdentifier): Promise<Readable> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
const onStoragePath = this.buildOnStoragePath(params);
|
||||
const { onStorageFilePath } = this.validateAndBuildFileStoragePath(params);
|
||||
|
||||
return driver.readFile({ filePath: onStoragePath });
|
||||
return driver.readFile({ filePath: onStorageFilePath });
|
||||
}
|
||||
|
||||
downloadFile(
|
||||
params: ResourceIdentifier & { localPath: string },
|
||||
): Promise<void> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
const onStoragePath = this.buildOnStoragePath(params);
|
||||
const { onStorageFilePath } = this.validateAndBuildFileStoragePath(params);
|
||||
|
||||
return driver.downloadFile({
|
||||
onStoragePath,
|
||||
onStoragePath: onStorageFilePath,
|
||||
localPath: params.localPath,
|
||||
});
|
||||
}
|
||||
@@ -179,17 +241,27 @@ export class FileStorageService {
|
||||
},
|
||||
});
|
||||
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
await driver.delete({
|
||||
folderPath: `${workspaceId}/${applicationUniversalIdentifier}/`,
|
||||
});
|
||||
|
||||
await this.fileRepository.delete({
|
||||
applicationId: application.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(params: ResourceIdentifier): Promise<void> {
|
||||
async deleteFile(params: ResourceIdentifier): Promise<void> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
const onStoragePath = this.buildOnStoragePath(params);
|
||||
const { onStorageFilePath, filePath } =
|
||||
this.validateAndBuildFileStoragePath(params);
|
||||
|
||||
const deleteResult = driver.delete({ folderPath: onStoragePath });
|
||||
await driver.delete({
|
||||
folderPath: dirname(onStorageFilePath),
|
||||
filename: basename(onStorageFilePath),
|
||||
});
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
@@ -198,18 +270,47 @@ export class FileStorageService {
|
||||
},
|
||||
});
|
||||
|
||||
const basePath = `${join(params.fileFolder, params.resourcePath)}`.replace(
|
||||
/\/+/g,
|
||||
'/',
|
||||
);
|
||||
|
||||
await this.fileRepository.delete({
|
||||
path: Like(`${basePath}%`),
|
||||
path: filePath,
|
||||
applicationId: application.id,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return deleteResult;
|
||||
async deleteFolder(
|
||||
params: Omit<ResourceIdentifier, 'resourcePath'> & { folderPath: string },
|
||||
): Promise<void> {
|
||||
const {
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
folderPath,
|
||||
} = params;
|
||||
|
||||
const { onStorageFolderPath, folderPath: validatedFolderPath } =
|
||||
this.validateAndBuildFolderStoragePath({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder,
|
||||
folderPath,
|
||||
});
|
||||
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
await driver.delete({ folderPath: onStorageFolderPath });
|
||||
|
||||
const application = await this.applicationRepository.findOneOrFail({
|
||||
where: {
|
||||
universalIdentifier: applicationUniversalIdentifier,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
await this.fileRepository.delete({
|
||||
path: Like(`${validatedFolderPath}%`),
|
||||
applicationId: application.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByFileId({
|
||||
@@ -233,14 +334,12 @@ export class FileStorageService {
|
||||
where: { id: file.applicationId, workspaceId: file.workspaceId },
|
||||
});
|
||||
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
await driver.delete({
|
||||
folderPath: `${file.workspaceId}/${application.universalIdentifier}`,
|
||||
filename: file.path,
|
||||
await this.deleteFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
fileFolder,
|
||||
resourcePath: removeFileFolderFromFileEntityPath(file.path),
|
||||
});
|
||||
|
||||
await this.fileRepository.delete(fileId);
|
||||
}
|
||||
|
||||
async checkIfWorkspaceFolderExists(workspaceId: string): Promise<boolean> {
|
||||
@@ -273,8 +372,10 @@ export class FileStorageService {
|
||||
}): Promise<void> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
const fromPath = this.buildOnStoragePath(from);
|
||||
const toPath = this.buildOnStoragePath(to);
|
||||
const { onStorageFilePath: fromPath } =
|
||||
this.validateAndBuildFileStoragePath(from);
|
||||
const { onStorageFilePath: toPath } =
|
||||
this.validateAndBuildFileStoragePath(to);
|
||||
|
||||
const isFile = await driver.checkFileExists({ filePath: fromPath });
|
||||
|
||||
@@ -293,8 +394,8 @@ export class FileStorageService {
|
||||
|
||||
checkFileExists(params: ResourceIdentifier): Promise<boolean> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
const onStoragePath = this.buildOnStoragePath(params);
|
||||
const { onStorageFilePath } = this.validateAndBuildFileStoragePath(params);
|
||||
|
||||
return driver.checkFileExists({ filePath: onStoragePath });
|
||||
return driver.checkFileExists({ filePath: onStorageFilePath });
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
type ResourcePathValidationSuccess = {
|
||||
isValid: true;
|
||||
};
|
||||
|
||||
type ResourcePathValidationFailure = {
|
||||
isValid: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ResourcePathValidationResult =
|
||||
| ResourcePathValidationSuccess
|
||||
| ResourcePathValidationFailure;
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { assertResourcePathIsSafe } from 'src/engine/core-modules/file-storage/utils/assert-resource-path-is-safe.util';
|
||||
|
||||
describe('assertResourcePathIsSafe', () => {
|
||||
it('should accept valid relative paths', () => {
|
||||
expect(() =>
|
||||
assertResourcePathIsSafe('src/components/test.mjs'),
|
||||
).not.toThrow();
|
||||
expect(() => assertResourcePathIsSafe('file.mjs')).not.toThrow();
|
||||
expect(() => assertResourcePathIsSafe('a/b/c/d.txt')).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject paths with .. traversal', () => {
|
||||
expect(() => assertResourcePathIsSafe('../../../other-ws/file.js')).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject absolute paths', () => {
|
||||
expect(() => assertResourcePathIsSafe('/etc/passwd')).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject paths with null bytes', () => {
|
||||
expect(() => assertResourcePathIsSafe('file\0.txt')).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject paths with backslashes', () => {
|
||||
expect(() => assertResourcePathIsSafe('..\\..\\etc\\passwd')).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject empty strings', () => {
|
||||
expect(() => assertResourcePathIsSafe('')).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { assertStoragePathIsWithinWorkspace } from 'src/engine/core-modules/file-storage/utils/assert-storage-path-is-within-workspace.util';
|
||||
|
||||
const primitives = {
|
||||
workspaceId: 'workspace-id',
|
||||
applicationUniversalIdentifier: 'app-uid',
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
};
|
||||
|
||||
describe('assertStoragePathIsWithinWorkspace', () => {
|
||||
it('should accept paths within the expected prefix', () => {
|
||||
expect(() =>
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
onStoragePath:
|
||||
'workspace-id/app-uid/built-front-component/src/component.mjs',
|
||||
...primitives,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept paths directly under the prefix', () => {
|
||||
expect(() =>
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
onStoragePath: 'workspace-id/app-uid/built-front-component/file.mjs',
|
||||
...primitives,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reject paths that escape via .. traversal', () => {
|
||||
expect(() =>
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
onStoragePath:
|
||||
'other-workspace/other-app/built-front-component/stolen.mjs',
|
||||
...primitives,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject paths that escape by one level', () => {
|
||||
expect(() =>
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
onStoragePath: 'workspace-id/app-uid/other-folder/file.mjs',
|
||||
...primitives,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject the prefix itself without a trailing file', () => {
|
||||
expect(() =>
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
onStoragePath: 'workspace-id/app-uid/built-front-component',
|
||||
...primitives,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject paths where prefix is a partial match', () => {
|
||||
expect(() =>
|
||||
assertStoragePathIsWithinWorkspace({
|
||||
onStoragePath:
|
||||
'workspace-id/app-uid/built-front-componentMalicious/file.mjs',
|
||||
...primitives,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { hasAllowedExtension } from 'src/engine/core-modules/file-storage/utils/has-allowed-extension.util';
|
||||
|
||||
describe('hasAllowedExtension', () => {
|
||||
const allowedExtensions = {
|
||||
'.mjs': true,
|
||||
'.ts': true,
|
||||
'.tsx': true,
|
||||
} as const;
|
||||
|
||||
it('should return true for an allowed extension', () => {
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'index.mjs', allowedExtensions }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'handler.ts', allowedExtensions }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'component.tsx', allowedExtensions }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for nested paths with allowed extensions', () => {
|
||||
expect(
|
||||
hasAllowedExtension({
|
||||
filePath: 'src/handlers/index.mjs',
|
||||
allowedExtensions,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasAllowedExtension({
|
||||
filePath: 'src/deep/nested/path/file.ts',
|
||||
allowedExtensions,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for disallowed extensions', () => {
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'index.js', allowedExtensions }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'index.html', allowedExtensions }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'script.sh', allowedExtensions }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for files with no extension', () => {
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'Makefile', allowedExtensions }),
|
||||
).toBe(false);
|
||||
expect(hasAllowedExtension({ filePath: 'README', allowedExtensions })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'index.MJS', allowedExtensions }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'handler.TS', allowedExtensions }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasAllowedExtension({ filePath: 'component.TSX', allowedExtensions }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should use the last extension for double extensions', () => {
|
||||
expect(
|
||||
hasAllowedExtension({
|
||||
filePath: 'archive.tar.gz',
|
||||
allowedExtensions: { '.gz': true },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
hasAllowedExtension({
|
||||
filePath: 'archive.tar.gz',
|
||||
allowedExtensions: { '.tar': true },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty file path', () => {
|
||||
expect(hasAllowedExtension({ filePath: '', allowedExtensions })).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { validateFileExtension } from 'src/engine/core-modules/file-storage/utils/validate-file-extension.util';
|
||||
|
||||
describe('validateFileExtension', () => {
|
||||
it.each([
|
||||
{
|
||||
title: 'BuiltLogicFunction with .mjs',
|
||||
resourcePath: 'src/handlers/index.mjs',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
},
|
||||
{
|
||||
title: 'BuiltFrontComponent with .mjs',
|
||||
resourcePath: 'src/components/card.mjs',
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
},
|
||||
{
|
||||
title: 'Source with .ts',
|
||||
resourcePath: 'src/index.ts',
|
||||
fileFolder: FileFolder.Source,
|
||||
},
|
||||
{
|
||||
title: 'Source with .tsx',
|
||||
resourcePath: 'src/app.tsx',
|
||||
fileFolder: FileFolder.Source,
|
||||
},
|
||||
{
|
||||
title: 'Dependencies with .json',
|
||||
resourcePath: 'package.json',
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
},
|
||||
{
|
||||
title: 'Dependencies with .lock',
|
||||
resourcePath: 'yarn.lock',
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
},
|
||||
])(
|
||||
'should return isValid: true for $title',
|
||||
({ resourcePath, fileFolder }) => {
|
||||
expect(validateFileExtension({ resourcePath, fileFolder })).toEqual({
|
||||
isValid: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: 'BuiltLogicFunction with .js',
|
||||
resourcePath: 'handler.js',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
},
|
||||
{
|
||||
title: 'BuiltFrontComponent with .html',
|
||||
resourcePath: 'component.html',
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
},
|
||||
{
|
||||
title: 'BuiltLogicFunction with .pdf',
|
||||
resourcePath: 'handler.pdf',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
},
|
||||
{
|
||||
title: 'Source with .mjs',
|
||||
resourcePath: 'src/index.mjs',
|
||||
fileFolder: FileFolder.Source,
|
||||
},
|
||||
{
|
||||
title: 'Dependencies with .sh',
|
||||
resourcePath: 'install.sh',
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
},
|
||||
])(
|
||||
'should return isValid: false for $title',
|
||||
({ resourcePath, fileFolder }) => {
|
||||
const result = validateFileExtension({ resourcePath, fileFolder });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('Invalid file extension');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: 'CorePicture',
|
||||
resourcePath: 'photo.png',
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
},
|
||||
{
|
||||
title: 'FilesField',
|
||||
resourcePath: 'document.pdf',
|
||||
fileFolder: FileFolder.FilesField,
|
||||
},
|
||||
{
|
||||
title: 'PublicAsset with .svg',
|
||||
resourcePath: 'assets/logo.svg',
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
},
|
||||
{
|
||||
title: 'PublicAsset with .js',
|
||||
resourcePath: 'assets/script.js',
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
},
|
||||
{
|
||||
title: 'PublicAsset with .exe',
|
||||
resourcePath: 'downloads/installer.exe',
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
},
|
||||
])(
|
||||
'should return isValid: true for unconfigured file folder $title',
|
||||
({ resourcePath, fileFolder }) => {
|
||||
expect(validateFileExtension({ resourcePath, fileFolder })).toEqual({
|
||||
isValid: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { validateFilePath } from 'src/engine/core-modules/file-storage/utils/validate-file-path.util';
|
||||
|
||||
describe('validateFilePath', () => {
|
||||
it.each([
|
||||
{
|
||||
title: 'valid built logic function path',
|
||||
resourcePath: 'src/handlers/index.mjs',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
},
|
||||
{
|
||||
title: 'valid source path',
|
||||
resourcePath: 'src/index.ts',
|
||||
fileFolder: FileFolder.Source,
|
||||
},
|
||||
{
|
||||
title: 'valid public asset path',
|
||||
resourcePath: 'assets/logo.svg',
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
},
|
||||
{
|
||||
title: 'valid dependencies path',
|
||||
resourcePath: 'package.json',
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
},
|
||||
{
|
||||
title: 'valid unconfigured folder',
|
||||
resourcePath: 'photo.png',
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
},
|
||||
])(
|
||||
'should return isValid: true for $title',
|
||||
({ resourcePath, fileFolder }) => {
|
||||
expect(validateFilePath({ resourcePath, fileFolder })).toEqual({
|
||||
isValid: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should fail on path traversal (safe relative path check)', () => {
|
||||
const result = validateFilePath({
|
||||
resourcePath: '../../../etc/passwd',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('path traversal');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on invalid characters (filename integrity check)', () => {
|
||||
const result = validateFilePath({
|
||||
resourcePath: 'my folder/file.mjs',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('invalid characters');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on missing extension (filename integrity check)', () => {
|
||||
const result = validateFilePath({
|
||||
resourcePath: 'Makefile',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('must have an extension');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on wrong extension (resource extension check)', () => {
|
||||
const result = validateFilePath({
|
||||
resourcePath: 'handler.js',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('Invalid file extension');
|
||||
}
|
||||
});
|
||||
|
||||
it('should short-circuit on the first failure', () => {
|
||||
const result = validateFilePath({
|
||||
resourcePath: '',
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('must not be empty');
|
||||
}
|
||||
});
|
||||
});
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { validateFolderPath } from 'src/engine/core-modules/file-storage/utils/validate-folder-path.util';
|
||||
|
||||
describe('validateFolderPath', () => {
|
||||
it.each([
|
||||
{ title: 'simple folder name', folderPath: 'my-folder' },
|
||||
{
|
||||
title: 'UUID folder',
|
||||
folderPath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
},
|
||||
{ title: 'nested folder path', folderPath: '0000-0999/tmp200/toto' },
|
||||
{ title: 'dot-prefixed hidden folder', folderPath: '.hidden' },
|
||||
])('should return isValid: true for $title', ({ folderPath }) => {
|
||||
expect(validateFolderPath({ folderPath })).toEqual({ isValid: true });
|
||||
});
|
||||
|
||||
it('should fail on path traversal', () => {
|
||||
const result = validateFolderPath({
|
||||
folderPath: '../../../other-ws/other-app/folder',
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('path traversal');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on absolute path', () => {
|
||||
const result = validateFolderPath({ folderPath: '/etc/secret-folder' });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('must be relative');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on empty folder path', () => {
|
||||
const result = validateFolderPath({ folderPath: '' });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('must not be empty');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on invalid characters (spaces)', () => {
|
||||
const result = validateFolderPath({ folderPath: 'my folder/data' });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('invalid characters');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail on shell metacharacters', () => {
|
||||
const result = validateFolderPath({ folderPath: 'folder;rm -rf' });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('invalid characters');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ title: 'trailing slash', folderPath: 'my-folder/' },
|
||||
{ title: 'double slashes', folderPath: 'my-folder//sub' },
|
||||
])('should fail on $title', ({ folderPath }) => {
|
||||
const result = validateFolderPath({ folderPath });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('empty segments or trailing slashes');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ title: '.mjs extension', folderPath: 'src/logic-functions/handler.mjs' },
|
||||
{ title: '.ts extension', folderPath: 'src/components/index.ts' },
|
||||
{ title: '.json extension', folderPath: 'config/settings.json' },
|
||||
{ title: '.woff2 extension', folderPath: 'fonts/roboto.woff2' },
|
||||
{ title: '.pdf extension', folderPath: 'docs/report.pdf' },
|
||||
{ title: '.csv extension', folderPath: 'data/export.csv' },
|
||||
{ title: 'numeric-only extension (.7z)', folderPath: 'archive.7z' },
|
||||
{ title: 'dotted version string (v1.0.0)', folderPath: 'v1.0.0' },
|
||||
{ title: 'numeric extension (.123)', folderPath: 'release.123' },
|
||||
])('should fail on path with file extension ($title)', ({ folderPath }) => {
|
||||
const result = validateFolderPath({ folderPath });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('file extension');
|
||||
}
|
||||
});
|
||||
|
||||
it('should short-circuit on the first failure', () => {
|
||||
const result = validateFolderPath({
|
||||
folderPath: '../../../handler.mjs',
|
||||
});
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain('path traversal');
|
||||
}
|
||||
});
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { validateSafeRelativePath } from 'src/engine/core-modules/file-storage/utils/validate-safe-relative-path.util';
|
||||
|
||||
describe('validateSafeRelativePath', () => {
|
||||
it.each([
|
||||
{ title: 'nested relative path', resourcePath: 'src/components/test.mjs' },
|
||||
{ title: 'simple filename', resourcePath: 'file.mjs' },
|
||||
{ title: 'deeply nested path', resourcePath: 'a/b/c/d.txt' },
|
||||
])('should return isValid: true for $title', ({ resourcePath }) => {
|
||||
expect(validateSafeRelativePath({ resourcePath })).toEqual({
|
||||
isValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: 'empty string',
|
||||
resourcePath: '',
|
||||
expectedError: 'must not be empty',
|
||||
},
|
||||
{
|
||||
title: 'null bytes',
|
||||
resourcePath: 'file\0.txt',
|
||||
expectedError: 'contains null bytes',
|
||||
},
|
||||
{
|
||||
title: 'absolute path',
|
||||
resourcePath: '/etc/passwd',
|
||||
expectedError: 'must be relative',
|
||||
},
|
||||
{
|
||||
title: 'backslashes',
|
||||
resourcePath: '..\\..\\etc\\passwd',
|
||||
expectedError: 'must not contain backslashes',
|
||||
},
|
||||
{
|
||||
title: '.. traversal',
|
||||
resourcePath: '../../../other-ws/file.js',
|
||||
expectedError: 'path traversal',
|
||||
},
|
||||
])(
|
||||
'should return isValid: false for $title',
|
||||
({ resourcePath, expectedError }) => {
|
||||
const result = validateSafeRelativePath({ resourcePath });
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
|
||||
if (!result.isValid) {
|
||||
expect(result.error).toContain(expectedError);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { validateStoragePathIsWithinWorkspaceOrThrow } from 'src/engine/core-modules/file-storage/utils/validate-storage-path-is-within-workspace-or-throw.util';
|
||||
|
||||
const primitives = {
|
||||
workspaceId: 'workspace-id',
|
||||
applicationUniversalIdentifier: 'app-uid',
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
};
|
||||
|
||||
describe('validateStoragePathIsWithinWorkspaceOrThrow', () => {
|
||||
it.each([
|
||||
{
|
||||
title: 'nested path within prefix',
|
||||
onStoragePath:
|
||||
'workspace-id/app-uid/built-front-component/src/component.mjs',
|
||||
},
|
||||
{
|
||||
title: 'file directly under prefix',
|
||||
onStoragePath: 'workspace-id/app-uid/built-front-component/file.mjs',
|
||||
},
|
||||
])('should accept valid path: $title', ({ onStoragePath }) => {
|
||||
expect(() =>
|
||||
validateStoragePathIsWithinWorkspaceOrThrow({
|
||||
onStoragePath,
|
||||
...primitives,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
title: 'different workspace and app',
|
||||
onStoragePath:
|
||||
'other-workspace/other-app/built-front-component/stolen.mjs',
|
||||
},
|
||||
{
|
||||
title: 'different file folder',
|
||||
onStoragePath: 'workspace-id/app-uid/other-folder/file.mjs',
|
||||
},
|
||||
{
|
||||
title: 'prefix without trailing file',
|
||||
onStoragePath: 'workspace-id/app-uid/built-front-component',
|
||||
},
|
||||
{
|
||||
title: 'partial prefix match (malicious suffix)',
|
||||
onStoragePath:
|
||||
'workspace-id/app-uid/built-front-componentMalicious/file.mjs',
|
||||
},
|
||||
])(
|
||||
'should reject path that escapes workspace: $title',
|
||||
({ onStoragePath }) => {
|
||||
expect(() =>
|
||||
validateStoragePathIsWithinWorkspaceOrThrow({
|
||||
onStoragePath,
|
||||
...primitives,
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FileStorageExceptionCode.ACCESS_DENIED,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { isSafeRelativePath } from 'src/engine/core-modules/file-storage/utils/is-safe-relative-path.util';
|
||||
|
||||
export const assertResourcePathIsSafe = (resourcePath: string): void => {
|
||||
if (!isSafeRelativePath(resourcePath)) {
|
||||
throw new FileStorageException(
|
||||
'Invalid resource path: contains unsafe characters or path traversal',
|
||||
FileStorageExceptionCode.ACCESS_DENIED,
|
||||
);
|
||||
}
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { extname } from 'path';
|
||||
|
||||
export const hasAllowedExtension = ({
|
||||
filePath,
|
||||
allowedExtensions,
|
||||
}: {
|
||||
filePath: string;
|
||||
allowedExtensions: Readonly<Record<string, true>>;
|
||||
}): boolean => {
|
||||
const ext = extname(filePath).toLowerCase();
|
||||
|
||||
return allowedExtensions[ext] === true;
|
||||
};
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { ALLOWED_EXTENSIONS_BY_APPLICATION_FILE_FOLDER } from 'src/engine/core-modules/file-storage/constants/allowed-extensions-by-application-file-folder.constant';
|
||||
import { type ResourcePathValidationResult } from 'src/engine/core-modules/file-storage/types/resource-path-validation-result.type';
|
||||
import { hasAllowedExtension } from 'src/engine/core-modules/file-storage/utils/has-allowed-extension.util';
|
||||
|
||||
export const validateFileExtension = ({
|
||||
resourcePath,
|
||||
fileFolder,
|
||||
}: {
|
||||
resourcePath: string;
|
||||
fileFolder: FileFolder;
|
||||
}): ResourcePathValidationResult => {
|
||||
const allowedExtensions =
|
||||
ALLOWED_EXTENSIONS_BY_APPLICATION_FILE_FOLDER[
|
||||
fileFolder as keyof typeof ALLOWED_EXTENSIONS_BY_APPLICATION_FILE_FOLDER
|
||||
];
|
||||
|
||||
if (!allowedExtensions) {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
if (
|
||||
!hasAllowedExtension({
|
||||
filePath: resourcePath,
|
||||
allowedExtensions,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Invalid file extension. Allowed extensions: ${Object.keys(allowedExtensions).join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { type ResourcePathValidationResult } from 'src/engine/core-modules/file-storage/types/resource-path-validation-result.type';
|
||||
import { validateFileExtension } from 'src/engine/core-modules/file-storage/utils/validate-file-extension.util';
|
||||
import { validatePathSegmentsSafety } from 'src/engine/core-modules/file-storage/utils/validate-path-segments-safety.util';
|
||||
import { validateSafeRelativePath } from 'src/engine/core-modules/file-storage/utils/validate-safe-relative-path.util';
|
||||
|
||||
export const validateFilePath = ({
|
||||
resourcePath,
|
||||
fileFolder,
|
||||
}: {
|
||||
resourcePath: string;
|
||||
fileFolder: FileFolder;
|
||||
}): ResourcePathValidationResult => {
|
||||
const safePathResult = validateSafeRelativePath({ resourcePath });
|
||||
|
||||
if (!safePathResult.isValid) {
|
||||
return safePathResult;
|
||||
}
|
||||
|
||||
const segmentsSafetyResult = validatePathSegmentsSafety({ resourcePath });
|
||||
|
||||
if (!segmentsSafetyResult.isValid) {
|
||||
return segmentsSafetyResult;
|
||||
}
|
||||
|
||||
const segments = resourcePath.split('/');
|
||||
const filename = segments[segments.length - 1];
|
||||
|
||||
if (!filename.includes('.')) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Filename must have an extension`,
|
||||
};
|
||||
}
|
||||
|
||||
return validateFileExtension({ resourcePath, fileFolder });
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { extname } from 'path';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { type ResourcePathValidationResult } from 'src/engine/core-modules/file-storage/types/resource-path-validation-result.type';
|
||||
import { validatePathSegmentsSafety } from 'src/engine/core-modules/file-storage/utils/validate-path-segments-safety.util';
|
||||
import { validateSafeRelativePath } from 'src/engine/core-modules/file-storage/utils/validate-safe-relative-path.util';
|
||||
|
||||
export const validateFolderPath = ({
|
||||
folderPath,
|
||||
}: {
|
||||
folderPath: string;
|
||||
}): ResourcePathValidationResult => {
|
||||
const safePathResult = validateSafeRelativePath({
|
||||
resourcePath: folderPath,
|
||||
});
|
||||
|
||||
if (!safePathResult.isValid) {
|
||||
return safePathResult;
|
||||
}
|
||||
|
||||
const segmentsSafetyResult = validatePathSegmentsSafety({
|
||||
resourcePath: folderPath,
|
||||
});
|
||||
|
||||
if (!segmentsSafetyResult.isValid) {
|
||||
return segmentsSafetyResult;
|
||||
}
|
||||
|
||||
const extension = extname(folderPath);
|
||||
|
||||
if (extension.length > 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Folder path must not contain a file extension — use deleteFile for file paths`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { type ResourcePathValidationResult } from 'src/engine/core-modules/file-storage/types/resource-path-validation-result.type';
|
||||
|
||||
const MAX_SEGMENT_LENGTH = 255;
|
||||
const MAX_PATH_LENGTH = 1024;
|
||||
const SAFE_SEGMENT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
||||
|
||||
export const validatePathSegmentsSafety = ({
|
||||
resourcePath,
|
||||
}: {
|
||||
resourcePath: string;
|
||||
}): ResourcePathValidationResult => {
|
||||
if (resourcePath.length > MAX_PATH_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Resource path exceeds maximum length of ${MAX_PATH_LENGTH} characters`,
|
||||
};
|
||||
}
|
||||
|
||||
if (resourcePath.includes('//') || resourcePath.endsWith('/')) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Resource path must not contain empty segments or trailing slashes`,
|
||||
};
|
||||
}
|
||||
|
||||
const segments = resourcePath.split('/');
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.length > MAX_SEGMENT_LENGTH) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`A path segment exceeds the maximum length of 255 characters`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!SAFE_SEGMENT_PATTERN.test(segment)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`A path segment contains invalid characters. Only alphanumeric, dots, dashes and underscores are allowed`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { isAbsolute, normalize, sep } from 'path';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { type ResourcePathValidationResult } from 'src/engine/core-modules/file-storage/types/resource-path-validation-result.type';
|
||||
|
||||
export const validateSafeRelativePath = ({
|
||||
resourcePath,
|
||||
}: {
|
||||
resourcePath: string;
|
||||
}): ResourcePathValidationResult => {
|
||||
if (resourcePath.length === 0) {
|
||||
return { isValid: false, error: t`Resource path must not be empty` };
|
||||
}
|
||||
|
||||
if (resourcePath.includes('\0')) {
|
||||
return { isValid: false, error: t`Resource path contains null bytes` };
|
||||
}
|
||||
|
||||
if (isAbsolute(resourcePath)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Resource path must be relative, not absolute`,
|
||||
};
|
||||
}
|
||||
|
||||
if (resourcePath.includes('\\')) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Resource path must not contain backslashes`,
|
||||
};
|
||||
}
|
||||
|
||||
const normalized = normalize(resourcePath);
|
||||
|
||||
if (normalized.split(sep).includes('..')) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: t`Resource path must not contain path traversal (..)`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
export const assertStoragePathIsWithinWorkspace = ({
|
||||
export const validateStoragePathIsWithinWorkspaceOrThrow = ({
|
||||
onStoragePath,
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
+1
-1
@@ -189,7 +189,7 @@ export class FileCorePictureService {
|
||||
const customApplicationUniversalIdentifier =
|
||||
await this.findCustomApplicationUniversalIdentifier(workspaceId);
|
||||
|
||||
await this.fileStorageService.delete({
|
||||
await this.fileStorageService.deleteFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier: customApplicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ export class LogicFunctionResourceService {
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
}: GetSourceCodeParams): Promise<void> {
|
||||
await this.fileStorageService.delete({
|
||||
await this.fileStorageService.deleteFile({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
fileFolder: FileFolder.Source,
|
||||
|
||||
+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,
|
||||
|
||||
+72
@@ -68,6 +68,78 @@ exports[`Upload application file should fail when filePath contains upward trave
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath has an invalid extension for BuiltFrontComponent (.js instead of .mjs) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Invalid input provided.",
|
||||
},
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath has an invalid extension for BuiltLogicFunction (.html) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Invalid input provided.",
|
||||
},
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath has an invalid extension for Dependencies (.sh instead of .json/.lock) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Invalid input provided.",
|
||||
},
|
||||
"message": "Invalid file extension. Allowed extensions: .json, .lock",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath has an invalid extension for Source (.js instead of .ts) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Invalid input provided.",
|
||||
},
|
||||
"message": "Invalid file extension. Allowed extensions: .ts, .tsx, .json",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath is a folder path without extension (bare UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Invalid input provided.",
|
||||
},
|
||||
"message": "Filename must have an extension",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath is a nested folder path without extension 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "INVALID_INPUT",
|
||||
"userFriendlyMessage": "Invalid input provided.",
|
||||
},
|
||||
"message": "Filename must have an extension",
|
||||
"name": "UserInputError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Upload application file should fail when filePath is an absolute path 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
|
||||
+53
@@ -84,6 +84,59 @@ const FAILING_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
filePath: 'src/components/legit.mjs',
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath is a folder path without extension (bare UUID)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'BuiltFrontComponent',
|
||||
filePath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath is a nested folder path without extension',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'Source',
|
||||
filePath: 'src/logic-functions/my-handler',
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath has an invalid extension for BuiltFrontComponent (.js instead of .mjs)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'BuiltFrontComponent',
|
||||
filePath: 'src/components/component.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath has an invalid extension for BuiltLogicFunction (.html)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'BuiltLogicFunction',
|
||||
filePath: 'src/handlers/handler.html',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when filePath has an invalid extension for Source (.js instead of .ts)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'Source',
|
||||
filePath: 'src/index.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath has an invalid extension for Dependencies (.sh instead of .json/.lock)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'Dependencies',
|
||||
filePath: 'install.sh',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Upload application file should fail', () => {
|
||||
|
||||
+189
-9
@@ -10,8 +10,8 @@ exports[`Front component creation should fail when builtComponentPath contains p
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -29,7 +29,115 @@ exports[`Front component creation should fail when builtComponentPath contains p
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when builtComponentPath has a completely unrelated extension (.pdf) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "PdfExtTest",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when builtComponentPath has an invalid extension (.js instead of .mjs) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "InvalidExtTest",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when builtComponentPath is a folder path without extension (bare UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "FolderPathTest",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
@@ -46,8 +154,8 @@ exports[`Front component creation should fail when builtComponentPath is an abso
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"message": "Resource path must be relative, not absolute",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -65,7 +173,7 @@ exports[`Front component creation should fail when builtComponentPath is an abso
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
@@ -154,8 +262,8 @@ exports[`Front component creation should fail when sourceComponentPath contains
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Source component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source component path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -173,7 +281,79 @@ exports[`Front component creation should fail when sourceComponentPath contains
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when sourceComponentPath has an invalid extension (.mjs instead of .ts/.tsx) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .ts, .tsx, .json",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "InvalidSourceExtTest",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component creation should fail when sourceComponentPath is a folder path without extension 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "FolderSourcePathTest",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating front component",
|
||||
"name": "GraphQLError",
|
||||
|
||||
+188
-13
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Front component update via manifest sync should fail for path traversal when builtComponentPath is updated with absolute path via manifest sync 1`] = `
|
||||
exports[`Front component update via manifest sync should fail for path traversal when builtComponentPath is a folder path without extension (bare UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
@@ -10,8 +10,8 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -28,7 +28,77 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component update via manifest sync should fail for path traversal when builtComponentPath is updated with a completely unrelated extension (.pdf) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component update via manifest sync should fail for path traversal when builtComponentPath is updated with absolute path via manifest sync 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Resource path must be relative, not absolute",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -45,8 +115,8 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"message": "Resource path must not contain backslashes",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -63,7 +133,42 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component update via manifest sync should fail for path traversal when builtComponentPath is updated with invalid extension (.js instead of .mjs) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -80,8 +185,8 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -98,7 +203,77 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component update via manifest sync should fail for path traversal when sourceComponentPath is a folder path without extension 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Front component update via manifest sync should fail for path traversal when sourceComponentPath is updated with invalid extension (.mjs instead of .ts/.tsx) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"frontComponent": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .ts, .tsx, .json",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "frontComponent",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 frontComponent",
|
||||
"summary": {
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -115,8 +290,8 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FRONT_COMPONENT_INPUT",
|
||||
"message": "Source component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source component path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -133,7 +308,7 @@ exports[`Front component update via manifest sync should fail for path traversal
|
||||
"frontComponent": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source component path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source component path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
|
||||
+65
@@ -74,6 +74,71 @@ const FAILING_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtComponentPath is a folder path without extension (bare UUID)',
|
||||
context: {
|
||||
input: {
|
||||
name: 'FolderPathTest',
|
||||
componentName: 'FolderPathTest',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceComponentPath is a folder path without extension',
|
||||
context: {
|
||||
input: {
|
||||
name: 'FolderSourcePathTest',
|
||||
componentName: 'FolderSourcePathTest',
|
||||
sourceComponentPath: 'src/front-components/my-component',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtComponentPath has an invalid extension (.js instead of .mjs)',
|
||||
context: {
|
||||
input: {
|
||||
name: 'InvalidExtTest',
|
||||
componentName: 'InvalidExtTest',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.js',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceComponentPath has an invalid extension (.mjs instead of .ts/.tsx)',
|
||||
context: {
|
||||
input: {
|
||||
name: 'InvalidSourceExtTest',
|
||||
componentName: 'InvalidSourceExtTest',
|
||||
sourceComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentPath: 'src/front-components/index.mjs',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtComponentPath has a completely unrelated extension (.pdf)',
|
||||
context: {
|
||||
input: {
|
||||
name: 'PdfExtTest',
|
||||
componentName: 'PdfExtTest',
|
||||
sourceComponentPath: 'src/front-components/index.tsx',
|
||||
builtComponentPath: 'src/front-components/index.pdf',
|
||||
builtComponentChecksum: 'abc123',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Front component creation should fail', () => {
|
||||
|
||||
+45
@@ -89,6 +89,51 @@ const FAILING_UPDATE_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtComponentPath is a folder path without extension (bare UUID)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtComponentPath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceComponentPath is a folder path without extension',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
sourceComponentPath: 'src/front-components/my-component',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtComponentPath is updated with invalid extension (.js instead of .mjs)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtComponentPath: 'src/front-components/test.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceComponentPath is updated with invalid extension (.mjs instead of .ts/.tsx)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
sourceComponentPath: 'src/front-components/test.mjs',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtComponentPath is updated with a completely unrelated extension (.pdf)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtComponentPath: 'src/front-components/test.pdf',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Front component update via manifest sync should fail for path traversal', () => {
|
||||
|
||||
+187
-12
@@ -10,8 +10,8 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"message": "Resource path must not contain backslashes",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -28,7 +28,7 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -45,8 +45,8 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -63,7 +63,112 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function creation via manifest sync should fail for path traversal when builtHandlerPath has a completely unrelated extension (.pdf) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function creation via manifest sync should fail for path traversal when builtHandlerPath has an invalid extension (.js instead of .mjs) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function creation via manifest sync should fail for path traversal when builtHandlerPath is a folder path without extension (bare UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -80,8 +185,8 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"message": "Resource path must be relative, not absolute",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -98,7 +203,7 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -115,8 +220,8 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Source handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source handler path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -133,7 +238,77 @@ exports[`Logic function creation via manifest sync should fail for path traversa
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function creation via manifest sync should fail for path traversal when sourceHandlerPath has an invalid extension (.mjs instead of .ts/.tsx) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .ts, .tsx, .json",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function creation via manifest sync should fail for path traversal when sourceHandlerPath is a folder path without extension 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
|
||||
+188
-13
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when builtHandlerPath is updated with absolute path via manifest sync 1`] = `
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when builtHandlerPath is a folder path without extension (bare UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
@@ -10,8 +10,8 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -28,7 +28,77 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when builtHandlerPath is updated with a completely unrelated extension (.pdf) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when builtHandlerPath is updated with absolute path via manifest sync 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Resource path must be relative, not absolute",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -45,8 +115,8 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"message": "Resource path must not contain backslashes",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -63,7 +133,42 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when builtHandlerPath is updated with invalid extension (.js instead of .mjs) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .mjs",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -80,8 +185,8 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -98,7 +203,77 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Built handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Built handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when sourceHandlerPath is a folder path without extension 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Filename must have an extension",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Logic function update via manifest sync should fail for path traversal when sourceHandlerPath is updated with invalid extension (.mjs instead of .ts/.tsx) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"logicFunction": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Invalid file extension. Allowed extensions: .ts, .tsx, .json",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "logicFunction",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 logicFunction",
|
||||
"summary": {
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
@@ -115,8 +290,8 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_LOGIC_FUNCTION_INPUT",
|
||||
"message": "Source handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source handler path contains unsafe characters",
|
||||
"message": "Resource path must not contain path traversal (..)",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
@@ -133,7 +308,7 @@ exports[`Logic function update via manifest sync should fail for path traversal
|
||||
"logicFunction": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Source handler path contains unsafe characters",
|
||||
"userFriendlyMessage": "Source handler path is invalid",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
|
||||
+42
@@ -81,6 +81,48 @@ const FAILING_CREATION_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when builtHandlerPath is a folder path without extension (bare UUID)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtHandlerPath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when sourceHandlerPath is a folder path without extension',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
sourceHandlerPath: 'src/logic-functions/my-handler',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtHandlerPath has an invalid extension (.js instead of .mjs)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtHandlerPath: 'src/logic-functions/handler.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceHandlerPath has an invalid extension (.mjs instead of .ts/.tsx)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
sourceHandlerPath: 'src/logic-functions/handler.mjs',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when builtHandlerPath has a completely unrelated extension (.pdf)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtHandlerPath: 'src/logic-functions/handler.pdf',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Logic function creation via manifest sync should fail for path traversal', () => {
|
||||
|
||||
+45
@@ -88,6 +88,51 @@ const FAILING_UPDATE_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtHandlerPath is a folder path without extension (bare UUID)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtHandlerPath: '8b2df3cc-23ad-4e1b-87fd-f880d4cefd58',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceHandlerPath is a folder path without extension',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
sourceHandlerPath: 'src/logic-functions/my-handler',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtHandlerPath is updated with invalid extension (.js instead of .mjs)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtHandlerPath: 'src/logic-functions/handler.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when sourceHandlerPath is updated with invalid extension (.mjs instead of .ts/.tsx)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
sourceHandlerPath: 'src/logic-functions/handler.mjs',
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when builtHandlerPath is updated with a completely unrelated extension (.pdf)',
|
||||
context: {
|
||||
manifest: buildManifest({
|
||||
builtHandlerPath: 'src/logic-functions/handler.pdf',
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('Logic function update via manifest sync should fail for path traversal', () => {
|
||||
|
||||
Reference in New Issue
Block a user