feat: adds restricted file check (#12989)
Adds `restrictedFileTypes` (default: `false`) to upload collections which prevents files on a restricted list from being uploaded. To skip this check: - set `[Collection].upload.restrictedFileTypes` to `true` - set `[Collection].upload.mimeType` to any type(s)
This commit is contained in:
@@ -110,6 +110,7 @@ _An asterisk denotes that an option is required._
|
||||
| **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Set to `true` to skip the safe fetch for all documents in this collection. Defaults to `false`. |
|
||||
| **`allowRestrictedFileTypes`** | Set to `true` to allow restricted file types. If your Collection has defined [mimeTypes](#mimetypes), restricted file verification will be skipped. Defaults to `false`. [More](#restricted-file-types) |
|
||||
| **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug |
|
||||
| **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) |
|
||||
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
|
||||
@@ -303,6 +304,47 @@ export const Media: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
## Restricted File Types
|
||||
|
||||
Possibly problematic file types are automatically restricted from being uploaded to your application.
|
||||
If your Collection has defined [mimeTypes](#mimetypes) or has set `allowRestrictedFileTypes` to `true`, restricted file verification will be skipped.
|
||||
|
||||
Restricted file types and extensions:
|
||||
|
||||
| File Extensions | MIME Type |
|
||||
| ------------------------------------ | ----------------------------------------------- |
|
||||
| `exe`, `dll` | `application/x-msdownload` |
|
||||
| `exe`, `com`, `app`, `action` | `application/x-executable` |
|
||||
| `bat`, `cmd` | `application/x-msdos-program` |
|
||||
| `exe`, `com` | `application/x-ms-dos-executable` |
|
||||
| `dmg` | `application/x-apple-diskimage` |
|
||||
| `deb` | `application/x-debian-package` |
|
||||
| `rpm` | `application/x-redhat-package-manager` |
|
||||
| `exe`, `dll` | `application/vnd.microsoft.portable-executable` |
|
||||
| `msi` | `application/x-msi` |
|
||||
| `jar`, `ear`, `war` | `application/java-archive` |
|
||||
| `desktop` | `application/x-desktop` |
|
||||
| `cpl` | `application/x-cpl` |
|
||||
| `lnk` | `application/x-ms-shortcut` |
|
||||
| `pkg` | `application/x-apple-installer` |
|
||||
| `htm`, `html`, `shtml`, `xhtml` | `text/html` |
|
||||
| `php`, `phtml` | `application/x-httpd-php` |
|
||||
| `js`, `jse` | `text/javascript` |
|
||||
| `jsp` | `application/x-jsp` |
|
||||
| `py` | `text/x-python` |
|
||||
| `rb` | `text/x-ruby` |
|
||||
| `pl` | `text/x-perl` |
|
||||
| `ps1`, `psc1`, `psd1`, `psh`, `psm1` | `application/x-powershell` |
|
||||
| `vbe`, `vbs` | `application/x-vbscript` |
|
||||
| `ws`, `wsc`, `wsf`, `wsh` | `application/x-ms-wsh` |
|
||||
| `scr` | `application/x-msdownload` |
|
||||
| `asp`, `aspx` | `application/x-asp` |
|
||||
| `hta` | `application/x-hta` |
|
||||
| `reg` | `application/x-registry` |
|
||||
| `url` | `application/x-url` |
|
||||
| `workflow` | `application/x-workflow` |
|
||||
| `command` | `application/x-command` |
|
||||
|
||||
## MimeTypes
|
||||
|
||||
Specifying the `mimeTypes` property can restrict what files are allowed from the user's file picker. This accepts an array of strings, which can be any valid mimetype or mimetype wildcards
|
||||
|
||||
77
packages/payload/src/uploads/checkFileRestrictions.ts
Normal file
77
packages/payload/src/uploads/checkFileRestrictions.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { checkFileRestrictionsParams, FileAllowList } from './types.js'
|
||||
|
||||
import { APIError } from '../errors/index.js'
|
||||
|
||||
/**
|
||||
* Restricted file types and their extensions.
|
||||
*/
|
||||
export const RESTRICTED_FILE_EXT_AND_TYPES: FileAllowList = [
|
||||
{ extensions: ['exe', 'dll'], mimeType: 'application/x-msdownload' },
|
||||
{ extensions: ['exe', 'com', 'app', 'action'], mimeType: 'application/x-executable' },
|
||||
{ extensions: ['bat', 'cmd'], mimeType: 'application/x-msdos-program' },
|
||||
{ extensions: ['exe', 'com'], mimeType: 'application/x-ms-dos-executable' },
|
||||
{ extensions: ['dmg'], mimeType: 'application/x-apple-diskimage' },
|
||||
{ extensions: ['deb'], mimeType: 'application/x-debian-package' },
|
||||
{ extensions: ['rpm'], mimeType: 'application/x-redhat-package-manager' },
|
||||
{ extensions: ['exe', 'dll'], mimeType: 'application/vnd.microsoft.portable-executable' },
|
||||
{ extensions: ['msi'], mimeType: 'application/x-msi' },
|
||||
{ extensions: ['jar', 'ear', 'war'], mimeType: 'application/java-archive' },
|
||||
{ extensions: ['desktop'], mimeType: 'application/x-desktop' },
|
||||
{ extensions: ['cpl'], mimeType: 'application/x-cpl' },
|
||||
{ extensions: ['lnk'], mimeType: 'application/x-ms-shortcut' },
|
||||
{ extensions: ['pkg'], mimeType: 'application/x-apple-installer' },
|
||||
{ extensions: ['htm', 'html', 'shtml', 'xhtml'], mimeType: 'text/html' },
|
||||
{ extensions: ['php', 'phtml'], mimeType: 'application/x-httpd-php' },
|
||||
{ extensions: ['js', 'jse'], mimeType: 'text/javascript' },
|
||||
{ extensions: ['jsp'], mimeType: 'application/x-jsp' },
|
||||
{ extensions: ['py'], mimeType: 'text/x-python' },
|
||||
{ extensions: ['rb'], mimeType: 'text/x-ruby' },
|
||||
{ extensions: ['pl'], mimeType: 'text/x-perl' },
|
||||
{ extensions: ['ps1', 'psc1', 'psd1', 'psh', 'psm1'], mimeType: 'application/x-powershell' },
|
||||
{ extensions: ['vbe', 'vbs'], mimeType: 'application/x-vbscript' },
|
||||
{ extensions: ['ws', 'wsc', 'wsf', 'wsh'], mimeType: 'application/x-ms-wsh' },
|
||||
{ extensions: ['scr'], mimeType: 'application/x-msdownload' },
|
||||
{ extensions: ['asp', 'aspx'], mimeType: 'application/x-asp' },
|
||||
{ extensions: ['hta'], mimeType: 'application/x-hta' },
|
||||
{ extensions: ['reg'], mimeType: 'application/x-registry' },
|
||||
{ extensions: ['url'], mimeType: 'application/x-url' },
|
||||
{ extensions: ['workflow'], mimeType: 'application/x-workflow' },
|
||||
{ extensions: ['command'], mimeType: 'application/x-command' },
|
||||
]
|
||||
|
||||
export const checkFileRestrictions = ({
|
||||
collection,
|
||||
file,
|
||||
req,
|
||||
}: checkFileRestrictionsParams): void => {
|
||||
const { upload: uploadConfig } = collection
|
||||
const configMimeTypes =
|
||||
uploadConfig &&
|
||||
typeof uploadConfig === 'object' &&
|
||||
'mimeTypes' in uploadConfig &&
|
||||
Array.isArray(uploadConfig.mimeTypes)
|
||||
? uploadConfig.mimeTypes
|
||||
: []
|
||||
|
||||
const allowRestrictedFileTypes =
|
||||
uploadConfig && typeof uploadConfig === 'object' && 'allowRestrictedFileTypes' in uploadConfig
|
||||
? (uploadConfig as { allowRestrictedFileTypes?: boolean }).allowRestrictedFileTypes
|
||||
: false
|
||||
|
||||
// Skip validation if `mimeTypes` are defined in the upload config, or `allowRestrictedFileTypes` are allowed
|
||||
if (allowRestrictedFileTypes || configMimeTypes.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const isRestricted = RESTRICTED_FILE_EXT_AND_TYPES.some((type) => {
|
||||
const hasRestrictedExt = type.extensions.some((ext) => file.name.toLowerCase().endsWith(ext))
|
||||
const hasRestrictedMime = type.mimeType === file.mimetype
|
||||
return hasRestrictedExt || hasRestrictedMime
|
||||
})
|
||||
|
||||
if (isRestricted) {
|
||||
const errorMessage = `File type '${file.mimetype}' not allowed ${file.name}: Restricted file type detected -- set 'allowRestrictedFileTypes' to true to skip this check for this Collection.`
|
||||
req.payload.logger.error(errorMessage)
|
||||
throw new APIError(errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type { FileData, FileToSave, ProbedImageSize, UploadEdits } from './types
|
||||
|
||||
import { FileRetrievalError, FileUploadError, Forbidden, MissingFile } from '../errors/index.js'
|
||||
import { canResizeImage } from './canResizeImage.js'
|
||||
import { checkFileRestrictions } from './checkFileRestrictions.js'
|
||||
import { cropImage } from './cropImage.js'
|
||||
import { getExternalFile } from './getExternalFile.js'
|
||||
import { getFileByPath } from './getFileByPath.js'
|
||||
@@ -19,7 +20,6 @@ import { getSafeFileName } from './getSafeFilename.js'
|
||||
import { resizeAndTransformImageSizes } from './imageResizer.js'
|
||||
import { isImage } from './isImage.js'
|
||||
import { optionallyAppendMetadata } from './optionallyAppendMetadata.js'
|
||||
|
||||
type Args<T> = {
|
||||
collection: Collection
|
||||
config: SanitizedConfig
|
||||
@@ -123,6 +123,12 @@ export const generateFileData = async <T>({
|
||||
}
|
||||
}
|
||||
|
||||
checkFileRestrictions({
|
||||
collection: collectionConfig,
|
||||
file,
|
||||
req,
|
||||
})
|
||||
|
||||
if (!disableLocalStorage) {
|
||||
await fs.mkdir(staticPath!, { recursive: true })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ResizeOptions, Sharp, SharpOptions } from 'sharp'
|
||||
|
||||
import type { TypeWithID } from '../collections/config/types.js'
|
||||
import type { CollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { PayloadComponent } from '../config/types.js'
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
import type { WithMetadata } from './optionallyAppendMetadata.js'
|
||||
@@ -102,6 +102,11 @@ export type AllowList = Array<{
|
||||
search?: string
|
||||
}>
|
||||
|
||||
export type FileAllowList = Array<{
|
||||
extensions: string[]
|
||||
mimeType: string
|
||||
}>
|
||||
|
||||
type Admin = {
|
||||
components?: {
|
||||
/**
|
||||
@@ -127,6 +132,14 @@ export type UploadConfig = {
|
||||
* - A function that generates a fully qualified URL for the thumbnail, receives the doc as the only argument.
|
||||
**/
|
||||
adminThumbnail?: GetAdminThumbnail | string
|
||||
/**
|
||||
* Allow restricted file types known to be problematic.
|
||||
* - If set to `true`, it will allow all file types.
|
||||
* - If set to `false`, it will not allow file types and extensions known to be problematic.
|
||||
* - This setting is overriden by the `mimeTypes` option.
|
||||
* @default false
|
||||
*/
|
||||
allowRestrictedFileTypes?: boolean
|
||||
/**
|
||||
* Enables bulk upload of files from the list view.
|
||||
* @default true
|
||||
@@ -221,7 +234,6 @@ export type UploadConfig = {
|
||||
* @default undefined
|
||||
*/
|
||||
modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers
|
||||
|
||||
/**
|
||||
* Controls the behavior of pasting/uploading files from URLs.
|
||||
* If set to `false`, fetching from remote URLs is disabled.
|
||||
@@ -263,6 +275,11 @@ export type UploadConfig = {
|
||||
*/
|
||||
withMetadata?: WithMetadata
|
||||
}
|
||||
export type checkFileRestrictionsParams = {
|
||||
collection: CollectionConfig
|
||||
file: File
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
export type SanitizedUploadConfig = {
|
||||
staticDir: UploadConfig['staticDir']
|
||||
|
||||
@@ -31,9 +31,12 @@ import {
|
||||
mediaWithoutCacheTagsSlug,
|
||||
mediaWithoutRelationPreviewSlug,
|
||||
mediaWithRelationPreviewSlug,
|
||||
noRestrictFileMimeTypesSlug,
|
||||
noRestrictFileTypesSlug,
|
||||
reduceSlug,
|
||||
relationPreviewSlug,
|
||||
relationSlug,
|
||||
restrictFileTypesSlug,
|
||||
skipAllowListSafeFetchMediaSlug,
|
||||
skipSafeFetchMediaSlug,
|
||||
threeDimensionalSlug,
|
||||
@@ -468,6 +471,27 @@ export default buildConfigWithDefaults({
|
||||
staticDir: path.resolve(dirname, './media'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: restrictFileTypesSlug,
|
||||
fields: [],
|
||||
upload: {
|
||||
allowRestrictedFileTypes: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: noRestrictFileTypesSlug,
|
||||
fields: [],
|
||||
upload: {
|
||||
allowRestrictedFileTypes: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: noRestrictFileMimeTypesSlug,
|
||||
fields: [],
|
||||
upload: {
|
||||
mimeTypes: ['text/html'],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: animatedTypeMedia,
|
||||
fields: [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
import { randomUUID } from 'crypto'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload'
|
||||
@@ -17,8 +18,11 @@ import {
|
||||
focalNoSizesSlug,
|
||||
focalOnlySlug,
|
||||
mediaSlug,
|
||||
noRestrictFileMimeTypesSlug,
|
||||
noRestrictFileTypesSlug,
|
||||
reduceSlug,
|
||||
relationSlug,
|
||||
restrictFileTypesSlug,
|
||||
skipAllowListSafeFetchMediaSlug,
|
||||
skipSafeFetchMediaSlug,
|
||||
unstoredMediaSlug,
|
||||
@@ -623,6 +627,49 @@ describe('Collections - Uploads', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('file restrictions', () => {
|
||||
const file: File = {
|
||||
name: `test-${randomUUID()}.html`,
|
||||
data: Buffer.from('<html><script>alert("test")</script></html>'),
|
||||
mimetype: 'text/html',
|
||||
size: 100,
|
||||
}
|
||||
it('should not allow files with restricted file types', async () => {
|
||||
await expect(async () =>
|
||||
payload.create({
|
||||
collection: restrictFileTypesSlug as CollectionSlug,
|
||||
data: {},
|
||||
file,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
name: 'APIError',
|
||||
message: `File type 'text/html' not allowed ${file.name}: Restricted file type detected -- set 'allowRestrictedFileTypes' to true to skip this check for this Collection.`,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow files with restricted file types when allowRestrictedFileTypes is true', async () => {
|
||||
await expect(
|
||||
payload.create({
|
||||
collection: noRestrictFileTypesSlug as CollectionSlug,
|
||||
data: {},
|
||||
file,
|
||||
}),
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('should allow files with restricted file types when mimeTypes are set', async () => {
|
||||
await expect(
|
||||
payload.create({
|
||||
collection: noRestrictFileMimeTypesSlug as CollectionSlug,
|
||||
data: {},
|
||||
file,
|
||||
}),
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('focal point', () => {
|
||||
|
||||
@@ -26,6 +26,9 @@ export const withoutMetadataSlug = 'without-meta-data'
|
||||
export const withOnlyJPEGMetadataSlug = 'with-only-jpeg-meta-data'
|
||||
export const customFileNameMediaSlug = 'custom-file-name-media'
|
||||
export const allowListMediaSlug = 'allow-list-media'
|
||||
export const restrictFileTypesSlug = 'restrict-file-types'
|
||||
export const noRestrictFileTypesSlug = 'no-restrict-file-types'
|
||||
export const noRestrictFileMimeTypesSlug = 'no-restrict-file-mime-types'
|
||||
export const skipSafeFetchMediaSlug = 'skip-safe-fetch-media'
|
||||
export const skipAllowListSafeFetchMediaSlug = 'skip-allow-list-safe-fetch-media'
|
||||
export const listViewPreviewSlug = 'list-view-preview'
|
||||
|
||||
Reference in New Issue
Block a user