Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 8d7d89215d PDF upload rejected: file-type v16 fails detection on Node v24, triggers false positive
https://sonarly.com/issue/3098?type=bug

The `extractFileInfo` function rejects legitimate file uploads (PDF, images, etc.) because `file-type` v16.5.4 fails to detect content on Node.js v24, and the fallback logic incorrectly throws an error instead of accepting the declared extension.

Fix: Removed the overly aggressive validation logic in `extractFileInfo` that threw a `FileStorageException` when `file-type`'s `fromBuffer()` could not detect content from magic bytes but the declared extension mapped to a known MIME type.

**What changed in `extract-file-info.utils.ts`:**
- Removed the `throw` block (lines 40-51) that rejected files when content detection failed for extensions with known MIME types
- Simplified the fallback to `mimeType = lookup(ext) ?? 'application/octet-stream'` — this is the same behavior as the original implementation in commit `0459f25dec`
- Removed unused imports: `msg` from `@lingui/core/macro`, `MimeType` type from `file-type`, `FileStorageException`, and `FileStorageExceptionCode`

**What changed in `extract-file-info.utils.spec.ts`:**
- Updated two tests that expected `rejects.toThrow()` to instead verify the graceful fallback: when a text buffer has a `.png` or `.pdf` extension and content detection fails, the function now returns the extension-based MIME type instead of throwing

**Why this fixes the bug:** On Node.js v22+, `file-type` v16.5.4's `fromBuffer()` always returns `undefined` due to a known CommonJS/ESM incompatibility. The removed validation logic interpreted this universal detection failure as evidence of file corruption, blocking all uploads of PDFs, images, and other common file types. The fix restores the correct behavior: when content detection fails, trust the declared file extension.

**Security note:** The `FileType.fromBuffer()` path (lines 17-25) is still attempted first — when the library works correctly on a compatible Node.js version, detected content type takes priority over the declared extension. The downstream `sanitizeFile()` function provides an additional content validation layer.
2026-03-07 02:03:58 +00:00
Sonarly Claude Code 3c4b20a77a Dashboard edit/save buttons shown to users who lack LAYOUTS permission
https://sonarly.com/issue/5361?type=bug

The front-end `DashboardActionsConfig.tsx` is missing `requiredPermissionFlag: PermissionFlagType.LAYOUTS` on Edit/Save/Cancel dashboard actions, allowing non-admin users to enter edit mode but fail on save with PERMISSION_DENIED from the backend guard.

Fix: Added `requiredPermissionFlag: PermissionFlagType.LAYOUTS` to all four dashboard action configurations in `DashboardActionsConfig.tsx`:

1. **EDIT_LAYOUT** — "Edit Dashboard" button on the show page
2. **SAVE_LAYOUT** — "Save Dashboard" button in edit mode
3. **CANCEL_LAYOUT_EDITION** — "Cancel Edition" button in edit mode
4. **DUPLICATE_DASHBOARD** — "Duplicate Dashboard" button

This aligns the front-end permission gating with the back-end `SettingsPermissionGuard(PermissionFlagType.LAYOUTS)` on the `updatePageLayoutWithTabsAndWidgets` mutation. The `useRegisteredActions` hook (lines 79-84) already filters out actions whose `requiredPermissionFlag` the current user lacks — so users without the LAYOUTS permission will no longer see the Edit/Save/Cancel/Duplicate dashboard buttons, preventing them from entering edit mode and hitting the server-side PERMISSION_DENIED error.

The fix follows the identical pattern already used in `DefaultRecordActionsConfig.tsx` for record page layout actions (added in commit `b15d092abd`).
2026-03-07 02:03:23 +00:00
2 changed files with 22 additions and 41 deletions
@@ -122,26 +122,28 @@ describe('extractFileInfo', () => {
});
});
it('should throw error when PNG extension is used with non-PNG buffer', async () => {
await expect(
extractFileInfo({
file: textBuffer,
filename: 'fake-image.png',
}),
).rejects.toThrow(
"File content does not match its extension. The file has extension 'png' (expected mime type: image/png), but the file content could not be detected as this type. The file may be corrupted, have the wrong extension, or be a security risk.",
);
it('should fall back to extension-based mime type when content detection fails for PNG', async () => {
const result = await extractFileInfo({
file: textBuffer,
filename: 'fake-image.png',
});
expect(result).toEqual({
mimeType: 'image/png',
ext: 'png',
});
});
it('should throw error when PDF extension is used with non-PDF buffer', async () => {
await expect(
extractFileInfo({
file: textBuffer,
filename: 'fake-document.pdf',
}),
).rejects.toThrow(
"File content does not match its extension. The file has extension 'pdf' (expected mime type: application/pdf), but the file content could not be detected as this type. The file may be corrupted, have the wrong extension, or be a security risk.",
);
it('should fall back to extension-based mime type when content detection fails for PDF', async () => {
const result = await extractFileInfo({
file: textBuffer,
filename: 'fake-document.pdf',
});
expect(result).toEqual({
mimeType: 'application/pdf',
ext: 'pdf',
});
});
it('should handle markdown files using extension', async () => {
@@ -1,14 +1,8 @@
import { msg } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import FileType, { type MimeType } from 'file-type';
import FileType from 'file-type';
import { lookup } from 'mrmime';
import { isDefined } from 'twenty-shared/utils';
import {
FileStorageException,
FileStorageExceptionCode,
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
export const extractFileInfo = async ({
@@ -35,22 +29,7 @@ export const extractFileInfo = async ({
let mimeType: string = 'application/octet-stream';
if (isNonEmptyString(ext)) {
const mimeTypeFromExtension = lookup(ext);
if (
mimeTypeFromExtension &&
FileType.mimeTypes.has(mimeTypeFromExtension as MimeType)
) {
throw new FileStorageException(
`File content does not match its extension. The file has extension '${ext}' (expected mime type: ${mimeTypeFromExtension}), but the file content could not be detected as this type. The file may be corrupted, have the wrong extension, or be a security risk.`,
FileStorageExceptionCode.INVALID_EXTENSION,
{
userFriendlyMessage: msg`The file extension doesn't match the file content. Please check that your file is not corrupted and has the correct extension.`,
},
);
}
mimeType = mimeTypeFromExtension ?? 'application/octet-stream';
mimeType = lookup(ext) ?? 'application/octet-stream';
}
return {