From c079ae79074253419272bfb4b309840282cd5cb2 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Tue, 24 Jun 2025 16:15:58 -0400 Subject: [PATCH 01/14] fix: implements allowlist --- .../payload/src/uploads/getExternalFile.ts | 54 +++++++++++++++++-- packages/payload/src/uploads/types.ts | 3 +- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index d932f45b5a8..9a880932e72 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -2,6 +2,7 @@ import type { PayloadRequest } from '../types/index.js' import type { File, FileData, UploadConfig } from './types.js' import { APIError } from '../errors/index.js' +import { isURLAllowed } from '../utilities/isURLAllowed.js' import { safeFetch } from './safeFetch.js' type Args = { @@ -23,11 +24,54 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers))) : { cookie: req.headers.get('cookie')! } - const res = await safeFetch(fileURL, { - credentials: 'include', - headers, - method: 'GET', - }) + /** + * We `fetch` on the `allowList` in the the upload config.` + * Otherwise we `safeFetch` + * Config example + * + * Allowlist format: + * ```ts + * Array<{ + hostname: string + pathname?: string + port?: string + protocol?: 'http' | 'https' + search?: string + }> + *``` + + * Config example: + * ```ts + * upload: { + pasteURL: { + allowList: [ + // Allow a specific URL + { protocol: 'https', hostname: 'example.com', port: '', search: '' }, + // Allow a specific URL with a port + { protocol: 'http', hostname: '127.0.0.1', port: '3000', search: '' }, + // Allow a local address + { protocol: 'http', hostname: 'localhost', port: '3000', search: '' }, + ], + }, + ``` + */ + const allowList = uploadConfig.pasteURL ? uploadConfig.pasteURL.allowList : [] + let res + if (allowList.length > 0 && isURLAllowed(fileURL, allowList)) { + // Allowed + res = await fetch(fileURL, { + credentials: 'include', + headers, + method: 'GET', + }) + } else { + // Default + res = await safeFetch(fileURL, { + credentials: 'include', + headers, + method: 'GET', + }) + } if (!res.ok) { throw new APIError(`Failed to fetch file from ${fileURL}`, res.status) diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index 869c974d0ee..b01b2a6d432 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -225,7 +225,8 @@ export type UploadConfig = { /** * Controls the behavior of pasting/uploading files from URLs. * If set to `false`, fetching from remote URLs is disabled. - * If an allowList is provided, server-side fetching will be enabled for specified URLs. + * If an `allowList` is provided, server-side fetching will be enabled for specified URLs. + * * @default true (client-side fetching enabled) */ pasteURL?: From e5f7192785d5f13678d8aab250ba6fa94f69fff1 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Tue, 24 Jun 2025 16:22:48 -0400 Subject: [PATCH 02/14] chore: doc --- packages/payload/src/uploads/getExternalFile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index 9a880932e72..61b28560324 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -25,8 +25,8 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis : { cookie: req.headers.get('cookie')! } /** - * We `fetch` on the `allowList` in the the upload config.` - * Otherwise we `safeFetch` + * `fetch` on the `allowList` in the the upload config.` + * Otherwise `safeFetch` * Config example * * Allowlist format: From 6499ecaaa8614f2881c53b050640ea90558726f0 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Wed, 25 Jun 2025 13:19:35 -0400 Subject: [PATCH 03/14] chore: adds allowList type and linting comments --- packages/payload/src/uploads/getExternalFile.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index 61b28560324..076671ef608 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -1,5 +1,5 @@ import type { PayloadRequest } from '../types/index.js' -import type { File, FileData, UploadConfig } from './types.js' +import type { AllowList, File, FileData, UploadConfig } from './types.js' import { APIError } from '../errors/index.js' import { isURLAllowed } from '../utilities/isURLAllowed.js' @@ -25,10 +25,10 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis : { cookie: req.headers.get('cookie')! } /** - * `fetch` on the `allowList` in the the upload config.` + * `fetch` on the `allowList` in the the upload config. * Otherwise `safeFetch` - * Config example - * + * Config example + * * Allowlist format: * ```ts * Array<{ @@ -39,7 +39,7 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis search?: string }> *``` - + * Config example: * ```ts * upload: { @@ -55,7 +55,7 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis }, ``` */ - const allowList = uploadConfig.pasteURL ? uploadConfig.pasteURL.allowList : [] + const allowList: AllowList = uploadConfig.pasteURL ? uploadConfig.pasteURL.allowList : [] let res if (allowList.length > 0 && isURLAllowed(fileURL, allowList)) { // Allowed From 164743156f6b05e4b030488a7864c3193b29c4ce Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Wed, 25 Jun 2025 14:52:00 -0400 Subject: [PATCH 04/14] chore: adds skipSafeFetch test --- test/uploads/config.ts | 11 +++++++++-- test/uploads/int.spec.ts | 19 ++++++++++++++++++- test/uploads/shared.ts | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/test/uploads/config.ts b/test/uploads/config.ts index 9bddc3a7545..c8375cdb4e4 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-restricted-exports */ - import type { CollectionSlug, File } from 'payload' import path from 'path' @@ -33,6 +31,7 @@ import { reduceSlug, relationPreviewSlug, relationSlug, + skipSafeFetchMediaSlug, threeDimensionalSlug, unstoredMediaSlug, versionSlug, @@ -429,6 +428,14 @@ export default buildConfigWithDefaults({ staticDir: path.resolve(dirname, './media'), }, }, + { + slug: skipSafeFetchMediaSlug, + fields: [], + upload: { + skipSafeFetch: true, + staticDir: path.resolve(dirname, './media'), + }, + }, { slug: animatedTypeMedia, fields: [], diff --git a/test/uploads/int.spec.ts b/test/uploads/int.spec.ts index f50eb79da3b..ba98ddb1e94 100644 --- a/test/uploads/int.spec.ts +++ b/test/uploads/int.spec.ts @@ -1,4 +1,4 @@ -import type { Payload } from 'payload' +import type { CollectionSlug, Payload } from 'payload' import fs from 'fs' import path from 'path' @@ -19,6 +19,7 @@ import { mediaSlug, reduceSlug, relationSlug, + skipSafeFetchMediaSlug, unstoredMediaSlug, usersSlug, } from './shared.js' @@ -585,6 +586,22 @@ describe('Collections - Uploads', () => { ) }, ) + it('should fetch when skipSafeFetch is enabled', async () => { + await expect( + payload.create({ + collection: skipSafeFetchMediaSlug as CollectionSlug, + data: { + filename: 'test.png', + url: 'http://127.0.0.1/file.png', + }, + }), + ).rejects.toThrow( + expect.objectContaining({ + name: 'FileRetrievalError', + message: expect.not.stringContaining('unsafe'), + }), + ) + }) }) }) diff --git a/test/uploads/shared.ts b/test/uploads/shared.ts index f334fd11dd5..30c672bdaae 100644 --- a/test/uploads/shared.ts +++ b/test/uploads/shared.ts @@ -25,7 +25,7 @@ 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 skipSafeFetchMediaSlug = 'skip-safe-fetch-media' export const listViewPreviewSlug = 'list-view-preview' export const threeDimensionalSlug = 'three-dimensional' export const constructorOptionsSlug = 'constructor-options' From 8df9e3b9c01581806b002d3c1d2b804e3e1ca0d0 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Wed, 25 Jun 2025 15:06:23 -0400 Subject: [PATCH 05/14] chore: updates docs --- docs/upload/overview.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index 99de802426d..ea51b446cdc 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -109,6 +109,7 @@ _An asterisk denotes that an option is required._ | **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | | **`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 `true` to skip the safe fetch check when fetching external files. Use `pasteURL.allowList` to specify a restricted list of allowed URLs. This is useful when retrieving external files stored on a cloud service. Defaults to `false`. | | **`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. | From 4a989521c05f5c399fcb42d48b5b8a5f54c0cc60 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Wed, 25 Jun 2025 15:06:45 -0400 Subject: [PATCH 06/14] chore: adds skipSafeFetch --- packages/payload/src/uploads/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index b01b2a6d432..e3aa43a1123 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -226,6 +226,7 @@ export type UploadConfig = { * Controls the behavior of pasting/uploading files from URLs. * If set to `false`, fetching from remote URLs is disabled. * If an `allowList` is provided, server-side fetching will be enabled for specified URLs. + * Without an `allowList`, the default external file retrieval behavior follows your `skipSafeFetch` option. * * @default true (client-side fetching enabled) */ @@ -240,6 +241,12 @@ export type UploadConfig = { * @default undefined */ resizeOptions?: ResizeOptions + /** + * Skip safe Fetch when fetching external files. + * If you want to allow specific URLS to be fetched without using `safeFetch`, you can use the `pasteURL.allowList` option. + * @default false + */ + skipSafeFetch?: boolean /** * The directory to serve static files from. Defaults to collection slug. * @default undefined From da43972a68f942afadcf831365a45bd3ecf43649 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Wed, 25 Jun 2025 15:07:50 -0400 Subject: [PATCH 07/14] chore: implements skipSafeFetch --- packages/payload/src/uploads/getExternalFile.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index 076671ef608..ad5f81ef414 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -24,6 +24,7 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers))) : { cookie: req.headers.get('cookie')! } + const skipSafeFetch: boolean = uploadConfig.skipSafeFetch || false /** * `fetch` on the `allowList` in the the upload config. * Otherwise `safeFetch` @@ -57,7 +58,7 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis */ const allowList: AllowList = uploadConfig.pasteURL ? uploadConfig.pasteURL.allowList : [] let res - if (allowList.length > 0 && isURLAllowed(fileURL, allowList)) { + if (skipSafeFetch || (allowList.length > 0 && isURLAllowed(fileURL, allowList))) { // Allowed res = await fetch(fileURL, { credentials: 'include', From 6aa6d6c5c87b2a9a5a002381234aeebff68a534d Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Wed, 25 Jun 2025 15:18:22 -0400 Subject: [PATCH 08/14] chore: allows default fetch for cloud storage --- packages/plugin-cloud-storage/src/plugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-cloud-storage/src/plugin.ts b/packages/plugin-cloud-storage/src/plugin.ts index 90b8daac241..0a0f1dda88d 100644 --- a/packages/plugin-cloud-storage/src/plugin.ts +++ b/packages/plugin-cloud-storage/src/plugin.ts @@ -92,6 +92,7 @@ export const cloudStoragePlugin = ? options.disableLocalStorage : true, handlers, + skipSafeFetch: true, }, } } From a04f909e4bddd76cd2e5331e52dd1a41753d7cb7 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Thu, 26 Jun 2025 10:30:38 -0400 Subject: [PATCH 09/14] chore: adds explicit allowList safe fetch check --- .../payload/src/uploads/getExternalFile.ts | 45 +++++-------------- packages/payload/src/uploads/types.ts | 8 ++-- packages/plugin-cloud-storage/src/plugin.ts | 9 +++- .../src/utilities/cloudStorageAllowList.ts | 12 +++++ 4 files changed, 34 insertions(+), 40 deletions(-) create mode 100644 packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index ad5f81ef414..87e540ca0bd 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -1,5 +1,5 @@ import type { PayloadRequest } from '../types/index.js' -import type { AllowList, File, FileData, UploadConfig } from './types.js' +import type { File, FileData, UploadConfig } from './types.js' import { APIError } from '../errors/index.js' import { isURLAllowed } from '../utilities/isURLAllowed.js' @@ -24,41 +24,18 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers))) : { cookie: req.headers.get('cookie')! } - const skipSafeFetch: boolean = uploadConfig.skipSafeFetch || false - /** - * `fetch` on the `allowList` in the the upload config. - * Otherwise `safeFetch` - * Config example - * - * Allowlist format: - * ```ts - * Array<{ - hostname: string - pathname?: string - port?: string - protocol?: 'http' | 'https' - search?: string - }> - *``` + // Check if URL is allowed because of skipSafeFetch allowList + const skipSafeFetch = + uploadConfig.skipSafeFetch && isURLAllowed(fileURL, uploadConfig.skipSafeFetch) + + // Check if URL is allowed because of pasteURL allowList + const isAllowedPasteUrl = + uploadConfig.pasteURL && + uploadConfig.pasteURL.allowList && + isURLAllowed(fileURL, uploadConfig.pasteURL.allowList) - * Config example: - * ```ts - * upload: { - pasteURL: { - allowList: [ - // Allow a specific URL - { protocol: 'https', hostname: 'example.com', port: '', search: '' }, - // Allow a specific URL with a port - { protocol: 'http', hostname: '127.0.0.1', port: '3000', search: '' }, - // Allow a local address - { protocol: 'http', hostname: 'localhost', port: '3000', search: '' }, - ], - }, - ``` - */ - const allowList: AllowList = uploadConfig.pasteURL ? uploadConfig.pasteURL.allowList : [] let res - if (skipSafeFetch || (allowList.length > 0 && isURLAllowed(fileURL, allowList))) { + if (skipSafeFetch || isAllowedPasteUrl) { // Allowed res = await fetch(fileURL, { credentials: 'include', diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index e3aa43a1123..893ae6dfe7e 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -226,7 +226,6 @@ export type UploadConfig = { * Controls the behavior of pasting/uploading files from URLs. * If set to `false`, fetching from remote URLs is disabled. * If an `allowList` is provided, server-side fetching will be enabled for specified URLs. - * Without an `allowList`, the default external file retrieval behavior follows your `skipSafeFetch` option. * * @default true (client-side fetching enabled) */ @@ -242,11 +241,10 @@ export type UploadConfig = { */ resizeOptions?: ResizeOptions /** - * Skip safe Fetch when fetching external files. - * If you want to allow specific URLS to be fetched without using `safeFetch`, you can use the `pasteURL.allowList` option. - * @default false + * Skip safe fetch when using server-side fetching for external files from these URLs. + * @default undefined */ - skipSafeFetch?: boolean + skipSafeFetch?: AllowList /** * The directory to serve static files from. Defaults to collection slug. * @default undefined diff --git a/packages/plugin-cloud-storage/src/plugin.ts b/packages/plugin-cloud-storage/src/plugin.ts index 0a0f1dda88d..0e4d10f9f23 100644 --- a/packages/plugin-cloud-storage/src/plugin.ts +++ b/packages/plugin-cloud-storage/src/plugin.ts @@ -5,6 +5,7 @@ import type { PluginOptions } from './types.js' import { getFields } from './fields/getFields.js' import { getAfterDeleteHook } from './hooks/afterDelete.js' import { getBeforeChangeHook } from './hooks/beforeChange.js' +import { cloudStorageAllowList } from './utilities/cloudStorageAllowList.js' // This plugin extends all targeted collections by offloading uploaded files // to cloud storage instead of solely storing files locally. @@ -92,7 +93,13 @@ export const cloudStoragePlugin = ? options.disableLocalStorage : true, handlers, - skipSafeFetch: true, + skipSafeFetch: [ + ...(typeof existingCollection.upload === 'object' && + Array.isArray(existingCollection.upload.skipSafeFetch) + ? existingCollection.upload.skipSafeFetch + : []), + ...cloudStorageAllowList, + ], }, } } diff --git a/packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts b/packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts new file mode 100644 index 00000000000..c52015bbea8 --- /dev/null +++ b/packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts @@ -0,0 +1,12 @@ +export type AllowList = Array<{ + hostname: string + pathname?: string + port?: string + protocol?: 'http' | 'https' + search?: string +}> + +export const cloudStorageAllowList: AllowList = [ + // Localhost + { hostname: 'localhost' }, +] From 721e1f4ff46a7c91053030d912e4c43d196892c3 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Thu, 26 Jun 2025 10:48:51 -0400 Subject: [PATCH 10/14] chore: updates docs --- docs/upload/overview.mdx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index ea51b446cdc..2b197ede35e 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -109,7 +109,7 @@ _An asterisk denotes that an option is required._ | **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | | **`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 `true` to skip the safe fetch check when fetching external files. Use `pasteURL.allowList` to specify a restricted list of allowed URLs. This is useful when retrieving external files stored on a cloud service. Defaults to `false`. | +| **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Defaults to `false`. | | **`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. | @@ -436,6 +436,24 @@ export const Media: CollectionConfig = { } ``` +You can also adjust server-side fetching at the upload level as well, this does not effect the `CORS` policy like the `pasteURL` option does, but it allows you to skip the safe fetch check for specific URLs. + +``` +import type { CollectionConfig } from 'payload' + +export const Media: CollectionConfig = { + slug: 'media', + upload: { + skipSafeFetch: [ + { + hostname: 'example.com', + pathname: '/images/*', + }, + ], + }, +} +``` + ##### Accepted Values for `pasteURL` | Option | Description | From d3dc21b5b9ac9c3de94af7d32f1ccfa186a3b268 Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Thu, 26 Jun 2025 13:28:14 -0400 Subject: [PATCH 11/14] chore: supports boolean or AllowList for skipSafeFetch --- .../payload/src/uploads/getExternalFile.ts | 9 ++-- packages/payload/src/uploads/types.ts | 4 +- packages/plugin-cloud-storage/src/plugin.ts | 42 +++++++++++++++---- packages/plugin-cloud-storage/src/types.ts | 8 ++++ .../src/utilities/cloudStorageAllowList.ts | 12 ------ 5 files changed, 49 insertions(+), 26 deletions(-) delete mode 100644 packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index 87e540ca0bd..50239beaff2 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -25,11 +25,14 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis : { cookie: req.headers.get('cookie')! } // Check if URL is allowed because of skipSafeFetch allowList - const skipSafeFetch = - uploadConfig.skipSafeFetch && isURLAllowed(fileURL, uploadConfig.skipSafeFetch) + const skipSafeFetch: boolean = + uploadConfig.skipSafeFetch === true + ? uploadConfig.skipSafeFetch + : Array.isArray(uploadConfig.skipSafeFetch) && + isURLAllowed(fileURL, uploadConfig.skipSafeFetch) // Check if URL is allowed because of pasteURL allowList - const isAllowedPasteUrl = + const isAllowedPasteUrl: boolean | undefined = uploadConfig.pasteURL && uploadConfig.pasteURL.allowList && isURLAllowed(fileURL, uploadConfig.pasteURL.allowList) diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index 893ae6dfe7e..004aad2c0e9 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -242,9 +242,9 @@ export type UploadConfig = { resizeOptions?: ResizeOptions /** * Skip safe fetch when using server-side fetching for external files from these URLs. - * @default undefined + * @default false */ - skipSafeFetch?: AllowList + skipSafeFetch?: AllowList | boolean /** * The directory to serve static files from. Defaults to collection slug. * @default undefined diff --git a/packages/plugin-cloud-storage/src/plugin.ts b/packages/plugin-cloud-storage/src/plugin.ts index 0e4d10f9f23..d6179ea0a25 100644 --- a/packages/plugin-cloud-storage/src/plugin.ts +++ b/packages/plugin-cloud-storage/src/plugin.ts @@ -1,11 +1,10 @@ import type { Config } from 'payload' -import type { PluginOptions } from './types.js' +import type { AllowList, PluginOptions } from './types.js' import { getFields } from './fields/getFields.js' import { getAfterDeleteHook } from './hooks/afterDelete.js' import { getBeforeChangeHook } from './hooks/beforeChange.js' -import { cloudStorageAllowList } from './utilities/cloudStorageAllowList.js' // This plugin extends all targeted collections by offloading uploaded files // to cloud storage instead of solely storing files locally. @@ -71,6 +70,37 @@ export const cloudStoragePlugin = }) } + const getSkipSafeFetchSetting = (): AllowList | boolean => { + if (options.disablePayloadAccessControl) { + return true + } + const isBooleanTrueSkipSafeFetch = + typeof existingCollection.upload === 'object' && + existingCollection.upload.skipSafeFetch === true + + const isAllowListSkipSafeFetch = + typeof existingCollection.upload === 'object' && + Array.isArray(existingCollection.upload.skipSafeFetch) + + if (isBooleanTrueSkipSafeFetch) { + return true + } else if (isAllowListSkipSafeFetch) { + return [ + ...(typeof existingCollection.upload === 'object' && + Array.isArray(existingCollection.upload.skipSafeFetch) + ? existingCollection.upload.skipSafeFetch + : []), + ...(process.env.NODE_ENV !== 'production' ? [{ hostname: 'localhost' }] : []), + ] + } + + if (process.env.NODE_ENV !== 'production') { + return [{ hostname: 'localhost' }] + } + + return false + } + return { ...existingCollection, fields, @@ -93,13 +123,7 @@ export const cloudStoragePlugin = ? options.disableLocalStorage : true, handlers, - skipSafeFetch: [ - ...(typeof existingCollection.upload === 'object' && - Array.isArray(existingCollection.upload.skipSafeFetch) - ? existingCollection.upload.skipSafeFetch - : []), - ...cloudStorageAllowList, - ], + skipSafeFetch: getSkipSafeFetchSetting(), }, } } diff --git a/packages/plugin-cloud-storage/src/types.ts b/packages/plugin-cloud-storage/src/types.ts index a1e1da9aee1..8558ccf7287 100644 --- a/packages/plugin-cloud-storage/src/types.ts +++ b/packages/plugin-cloud-storage/src/types.ts @@ -81,6 +81,14 @@ export interface GeneratedAdapter { export type Adapter = (args: { collection: CollectionConfig; prefix?: string }) => GeneratedAdapter +export type AllowList = Array<{ + hostname: string + pathname?: string + port?: string + protocol?: 'http' | 'https' + search?: string +}> + export type GenerateFileURL = (args: { collection: CollectionConfig filename: string diff --git a/packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts b/packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts deleted file mode 100644 index c52015bbea8..00000000000 --- a/packages/plugin-cloud-storage/src/utilities/cloudStorageAllowList.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type AllowList = Array<{ - hostname: string - pathname?: string - port?: string - protocol?: 'http' | 'https' - search?: string -}> - -export const cloudStorageAllowList: AllowList = [ - // Localhost - { hostname: 'localhost' }, -] From 5ffbacbfb947042e668f900e9d070ee6f15affeb Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Thu, 26 Jun 2025 13:56:19 -0400 Subject: [PATCH 12/14] chore: dedupes possible entries --- packages/plugin-cloud-storage/src/plugin.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/plugin-cloud-storage/src/plugin.ts b/packages/plugin-cloud-storage/src/plugin.ts index d6179ea0a25..56ce44a1b11 100644 --- a/packages/plugin-cloud-storage/src/plugin.ts +++ b/packages/plugin-cloud-storage/src/plugin.ts @@ -85,13 +85,19 @@ export const cloudStoragePlugin = if (isBooleanTrueSkipSafeFetch) { return true } else if (isAllowListSkipSafeFetch) { - return [ - ...(typeof existingCollection.upload === 'object' && + const existingSkipSafeFetch = + typeof existingCollection.upload === 'object' && Array.isArray(existingCollection.upload.skipSafeFetch) ? existingCollection.upload.skipSafeFetch - : []), - ...(process.env.NODE_ENV !== 'production' ? [{ hostname: 'localhost' }] : []), - ] + : [] + + const localhostEntry = + process.env.NODE_ENV !== 'production' && + !existingSkipSafeFetch.some((entry) => entry.hostname === 'localhost') + ? [{ hostname: 'localhost' }] + : [] + + return [...existingSkipSafeFetch, ...localhostEntry] } if (process.env.NODE_ENV !== 'production') { From 757a5ccddf9aa2fe9f4b0764bcfee93fb13ace6c Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Thu, 26 Jun 2025 14:03:32 -0400 Subject: [PATCH 13/14] chore: docs --- docs/upload/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index 2b197ede35e..2de286af3df 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -109,7 +109,7 @@ _An asterisk denotes that an option is required._ | **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | | **`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. Defaults to `false`. | +| **`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`. | | **`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. | From 9e801a856302ef5091c3eea7308aa973542c05ed Mon Sep 17 00:00:00 2001 From: Kendell Joseph Date: Thu, 26 Jun 2025 14:16:55 -0400 Subject: [PATCH 14/14] chore: dedupes only a specific entry --- packages/plugin-cloud-storage/src/plugin.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugin-cloud-storage/src/plugin.ts b/packages/plugin-cloud-storage/src/plugin.ts index 56ce44a1b11..1d0bacbe502 100644 --- a/packages/plugin-cloud-storage/src/plugin.ts +++ b/packages/plugin-cloud-storage/src/plugin.ts @@ -91,9 +91,13 @@ export const cloudStoragePlugin = ? existingCollection.upload.skipSafeFetch : [] + const hasExactLocalhostMatch = existingSkipSafeFetch.some((entry) => { + const entryKeys = Object.keys(entry) + return entryKeys.length === 1 && entry.hostname === 'localhost' + }) + const localhostEntry = - process.env.NODE_ENV !== 'production' && - !existingSkipSafeFetch.some((entry) => entry.hostname === 'localhost') + process.env.NODE_ENV !== 'production' && !hasExactLocalhostMatch ? [{ hostname: 'localhost' }] : []