Early return in public assets (#20881)

# Introduction
Related https://github.com/twentyhq/twenty/issues/20879

More abstracted response error and cleaner integrity check before
performing any in database search
Nothing critical patched here

Also added integration coverage to the related endpoint

Fixed the stream on error throw that would have been bubbling up into
node process

## Next
Once this has been approved will re-apply to all the existing prone
file.getBy* methods and controllers endpoints
This commit is contained in:
Paul Rastoin
2026-05-25 16:17:32 +02:00
committed by GitHub
parent be39702fd2
commit 69d89f8cfc
7 changed files with 516 additions and 52 deletions
@@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Public assets controller download should fail when path attempts to escape into another file folder 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
exports[`Public assets controller download should fail when path contains URL-encoded backslash traversal 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
exports[`Public assets controller download should fail when path contains a leading parent-directory segment (..) 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
exports[`Public assets controller download should fail when path contains multiple upward traversal segments (../../) 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
exports[`Public assets controller download should fail when the applicationId does not match any application 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
exports[`Public assets controller download should fail when the requested asset does not exist 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
exports[`Public assets controller download should fail when the workspaceId does not match any workspace 1`] = `
{
"body": {
"code": "FILE_NOT_FOUND",
"error": "Error",
"messages": [
"File not found",
],
"statusCode": 404,
},
"status": 404,
}
`;
@@ -0,0 +1,141 @@
import request from 'supertest';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
import { expectOneNotInternalServerErrorHttpResponseSnapshot } from 'test/integration/utils/expect-one-not-internal-server-error-http-response-snapshot.util';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_UID = uuidv4();
const TEST_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const UNKNOWN_WORKSPACE_ID = uuidv4();
const UNKNOWN_APPLICATION_ID = uuidv4();
const PUBLIC_ASSET_PATH = 'assets/logo.svg';
const PUBLIC_ASSET_CONTENT = '<svg><circle r="10" /></svg>';
const PUBLIC_ASSET_CONTENT_TYPE = 'image/svg+xml';
type FailingCase = {
buildUrl: (validApplicationId: string) => string;
};
const FAILING_CASES: EachTestingContext<FailingCase>[] = [
{
title: 'when path contains a leading parent-directory segment (..)',
context: {
buildUrl: (applicationId) =>
`/public-assets/${TEST_WORKSPACE_ID}/${applicationId}/..`,
},
},
{
title: 'when path contains multiple upward traversal segments (../../)',
context: {
buildUrl: (applicationId) =>
`/public-assets/${TEST_WORKSPACE_ID}/${applicationId}/../../sensitive-file`,
},
},
{
title: 'when path attempts to escape into another file folder',
context: {
buildUrl: (applicationId) =>
`/public-assets/${TEST_WORKSPACE_ID}/${applicationId}/../workflow/secret.json`,
},
},
{
title: 'when path contains URL-encoded backslash traversal',
context: {
buildUrl: (applicationId) =>
`/public-assets/${TEST_WORKSPACE_ID}/${applicationId}/..%5C..%5Cetc%5Cpasswd`,
},
},
{
title: 'when the requested asset does not exist',
context: {
buildUrl: (applicationId) =>
`/public-assets/${TEST_WORKSPACE_ID}/${applicationId}/assets/does-not-exist.svg`,
},
},
{
title: 'when the workspaceId does not match any workspace',
context: {
buildUrl: (applicationId) =>
`/public-assets/${UNKNOWN_WORKSPACE_ID}/${applicationId}/${PUBLIC_ASSET_PATH}`,
},
},
{
title: 'when the applicationId does not match any application',
context: {
buildUrl: () =>
`/public-assets/${TEST_WORKSPACE_ID}/${UNKNOWN_APPLICATION_ID}/${PUBLIC_ASSET_PATH}`,
},
},
];
describe('Public assets controller download should fail', () => {
let applicationId: string;
beforeAll(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_UID,
name: 'Test Public Assets Download Failure App',
description:
'App for testing failing public-assets controller downloads',
sourcePath: 'test-public-assets-download-failure',
});
const [{ id }] = await globalThis.testDataSource.query(
`SELECT id FROM core."application" WHERE "universalIdentifier" = $1`,
[TEST_APP_UID],
);
applicationId = id;
// A real public asset must exist so the "no content leak" guard below is
// meaningful — without it, `not.toContain(PUBLIC_ASSET_CONTENT)` would
// pass trivially regardless of the controller's behavior.
jest.useRealTimers();
await uploadApplicationFile({
applicationUniversalIdentifier: TEST_APP_UID,
fileFolder: 'PublicAsset',
filePath: PUBLIC_ASSET_PATH,
fileBuffer: Buffer.from(PUBLIC_ASSET_CONTENT),
filename: 'logo.svg',
contentType: PUBLIC_ASSET_CONTENT_TYPE,
expectToFail: false,
});
jest.useFakeTimers();
}, 60000);
afterAll(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: TEST_APP_UID,
});
});
it.each(eachTestingContextFilter(FAILING_CASES))(
'$title',
async ({ context }) => {
jest.useRealTimers();
const response = await request(global.app.getHttpServer()).get(
context.buildUrl(applicationId),
);
jest.useFakeTimers();
// The legitimate asset content must never leak through a failure path.
expect(response.text).not.toContain(PUBLIC_ASSET_CONTENT);
expectOneNotInternalServerErrorHttpResponseSnapshot({
status: response.status,
body: response.body,
});
},
30000,
);
});
@@ -0,0 +1,82 @@
import request from 'supertest';
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_UID = uuidv4();
const TEST_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const PUBLIC_ASSET_PATH = 'assets/logo.svg';
const PUBLIC_ASSET_CONTENT = '<svg><circle r="10" /></svg>';
const PUBLIC_ASSET_CONTENT_TYPE = 'image/svg+xml';
describe('Public assets controller download should succeed', () => {
let applicationId: string;
beforeAll(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_UID,
name: 'Test Public Assets Download Success App',
description:
'App for testing successful public-assets controller downloads',
sourcePath: 'test-public-assets-download-success',
});
const [{ id }] = await globalThis.testDataSource.query(
`SELECT id FROM core."application" WHERE "universalIdentifier" = $1`,
[TEST_APP_UID],
);
applicationId = id;
jest.useRealTimers();
await uploadApplicationFile({
applicationUniversalIdentifier: TEST_APP_UID,
fileFolder: 'PublicAsset',
filePath: PUBLIC_ASSET_PATH,
fileBuffer: Buffer.from(PUBLIC_ASSET_CONTENT),
filename: 'logo.svg',
contentType: PUBLIC_ASSET_CONTENT_TYPE,
expectToFail: false,
});
jest.useFakeTimers();
}, 60000);
afterAll(async () => {
await cleanupApplicationAndAppRegistration({
applicationUniversalIdentifier: TEST_APP_UID,
});
});
it('should stream a public asset with the correct headers and body', async () => {
jest.useRealTimers();
// `image/svg+xml` is treated as binary by superagent: the bytes land in
// `response.body` as a Buffer, and `response.text` stays undefined.
const response = await request(global.app.getHttpServer())
.get(
`/public-assets/${TEST_WORKSPACE_ID}/${applicationId}/${PUBLIC_ASSET_PATH}`,
)
.buffer(true)
.parse((res, callback) => {
const chunks: Buffer[] = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => callback(null, Buffer.concat(chunks)));
});
jest.useFakeTimers();
expect(response.status).toBe(200);
expect(response.headers['content-type']).toContain(
PUBLIC_ASSET_CONTENT_TYPE,
);
expect(response.headers['x-content-type-options']).toBe('nosniff');
expect((response.body as Buffer).toString('utf-8')).toBe(
PUBLIC_ASSET_CONTENT,
);
}, 30000);
});
@@ -0,0 +1,13 @@
// REST counterpart of `expectOneNotInternalServerErrorSnapshot`.
export const expectOneNotInternalServerErrorHttpResponseSnapshot = ({
status,
body,
}: {
status: number;
body: Record<string, unknown>;
}) => {
expect(status).not.toBe(500);
expect(body.code).not.toBe('INTERNAL_SERVER_ERROR');
expect({ status, body }).toMatchSnapshot();
};