diff --git a/docs/upload/overview.mdx b/docs/upload/overview.mdx index 2c8218eba6b..6b0b058c386 100644 --- a/docs/upload/overview.mdx +++ b/docs/upload/overview.mdx @@ -90,33 +90,33 @@ export const Media: CollectionConfig = { _An asterisk denotes that an option is required._ -| Option | Description | -| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) | -| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true | -| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. | -| **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) | -| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) | -| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | -| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). | -| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. | -| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. | -| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. | -| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) | -| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) | -| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. | -| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | -| **`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. 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. | -| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. | -| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. | -| **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) | +| Option | Description | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) | +| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true | +| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. | +| **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) | +| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) | +| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | +| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). | +| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. If using this option, you should handle the removal of any sensitive cookies (like payload-prefixed cookies) to prevent leaking session information to external services. By default, Payload automatically filters out payload-prefixed cookies when this option is not defined. | +| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. | +| **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. | +| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) | +| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) | +| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. | +| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | +| **`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. 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. | +| **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. | +| **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. | +| **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) | ### Payload-wide Upload Options diff --git a/packages/payload/src/uploads/getExternalFile.ts b/packages/payload/src/uploads/getExternalFile.ts index 50239beaff2..9cdafd68f8f 100644 --- a/packages/payload/src/uploads/getExternalFile.ts +++ b/packages/payload/src/uploads/getExternalFile.ts @@ -22,7 +22,14 @@ export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promis const headers = uploadConfig.externalFileHeaderFilter ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers))) - : { cookie: req.headers.get('cookie')! } + : { + cookie: + req.headers + .get('cookie') + ?.split(';') + .filter((cookie) => !cookie.trim().startsWith(req.payload.config.cookiePrefix)) + .join(';') || '', + } // Check if URL is allowed because of skipSafeFetch allowList const skipSafeFetch: boolean = diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index ff4963833a0..60f17d4dd26 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -173,7 +173,12 @@ export type UploadConfig = { */ displayPreview?: boolean /** - * Ability to filter/modify Request Headers when fetching a file. + * + * Accepts existing headers and returns the headers after filtering or modifying. + * If using this option, you should handle the removal of any sensitive cookies + * (like payload-prefixed cookies) to prevent leaking session information to external + * services. By default, Payload automatically filters out payload-prefixed cookies + * when this option is NOT defined. * * Useful for adding custom headers to fetch from external providers. * @default undefined diff --git a/test/uploads/config.ts b/test/uploads/config.ts index dbf3771ca18..f3c5f8a9be6 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -39,6 +39,7 @@ import { relationSlug, restrictFileTypesSlug, skipAllowListSafeFetchMediaSlug, + skipSafeFetchHeaderFilterSlug, skipSafeFetchMediaSlug, svgOnlySlug, threeDimensionalSlug, @@ -465,6 +466,15 @@ export default buildConfigWithDefaults({ staticDir: path.resolve(dirname, './media'), }, }, + { + slug: skipSafeFetchHeaderFilterSlug, + fields: [], + upload: { + skipSafeFetch: true, + staticDir: path.resolve(dirname, './media'), + externalFileHeaderFilter: (headers) => headers, // Keep all headers including cookies + }, + }, { slug: skipAllowListSafeFetchMediaSlug, fields: [], diff --git a/test/uploads/int.spec.ts b/test/uploads/int.spec.ts index bb6ceace31e..c2952b45aef 100644 --- a/test/uploads/int.spec.ts +++ b/test/uploads/int.spec.ts @@ -24,6 +24,7 @@ import { relationSlug, restrictFileTypesSlug, skipAllowListSafeFetchMediaSlug, + skipSafeFetchHeaderFilterSlug, skipSafeFetchMediaSlug, svgOnlySlug, unstoredMediaSlug, @@ -567,6 +568,68 @@ describe('Collections - Uploads', () => { }) }) + describe('cookie filtering', () => { + it('should filter out payload cookies when externalFileHeaderFilter is not defined', async () => { + const testCookies = ['payload-token=123', 'other-cookie=456', 'payload-something=789'].join( + '; ', + ) + + const fetchSpy = jest.spyOn(global, 'fetch') + + await payload.create({ + collection: skipSafeFetchMediaSlug, + data: { + filename: 'fat-head-nate.png', + url: 'https://www.payload.marketing/fat-head-nate.png', + }, + req: { + headers: new Headers({ + cookie: testCookies, + }), + }, + }) + + const [[, options]] = fetchSpy.mock.calls + const cookieHeader = options.headers.cookie + + expect(cookieHeader).not.toContain('payload-token=123') + expect(cookieHeader).not.toContain('payload-something=789') + expect(cookieHeader).toContain('other-cookie=456') + + fetchSpy.mockRestore() + }) + + it('should keep all cookies when externalFileHeaderFilter is defined', async () => { + const testCookies = ['payload-token=123', 'other-cookie=456', 'payload-something=789'].join( + '; ', + ) + + const fetchSpy = jest.spyOn(global, 'fetch') + + await payload.create({ + collection: skipSafeFetchHeaderFilterSlug, + data: { + filename: 'fat-head-nate.png', + url: 'https://www.payload.marketing/fat-head-nate.png', + }, + req: { + headers: new Headers({ + cookie: testCookies, + }), + }, + }) + + const [[, options]] = fetchSpy.mock.calls + const cookieHeader = options.headers.cookie + + expect(cookieHeader).toContain('other-cookie=456') + expect(cookieHeader).toContain('payload-token=123') + expect(cookieHeader).toContain('payload-something=789') + + fetchSpy.mockRestore() + }) + }) + describe('filters', () => { it.each` url | collection | errorContains diff --git a/test/uploads/payload-types.ts b/test/uploads/payload-types.ts index 6e807e723ea..d4ccd00d233 100644 --- a/test/uploads/payload-types.ts +++ b/test/uploads/payload-types.ts @@ -83,6 +83,7 @@ export interface Config { media: Media; 'allow-list-media': AllowListMedia; 'skip-safe-fetch-media': SkipSafeFetchMedia; + 'skip-safe-fetch-header-filter': SkipSafeFetchHeaderFilter; 'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMedia; 'restrict-file-types': RestrictFileType; 'no-restrict-file-types': NoRestrictFileType; @@ -141,6 +142,7 @@ export interface Config { media: MediaSelect | MediaSelect; 'allow-list-media': AllowListMediaSelect | AllowListMediaSelect; 'skip-safe-fetch-media': SkipSafeFetchMediaSelect | SkipSafeFetchMediaSelect; + 'skip-safe-fetch-header-filter': SkipSafeFetchHeaderFilterSelect | SkipSafeFetchHeaderFilterSelect; 'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMediaSelect | SkipAllowListSafeFetchMediaSelect; 'restrict-file-types': RestrictFileTypesSelect | RestrictFileTypesSelect; 'no-restrict-file-types': NoRestrictFileTypesSelect | NoRestrictFileTypesSelect; @@ -830,6 +832,24 @@ export interface SkipSafeFetchMedia { focalX?: number | null; focalY?: number | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "skip-safe-fetch-header-filter". + */ +export interface SkipSafeFetchHeaderFilter { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "skip-allow-list-safe-fetch-media". @@ -1698,6 +1718,10 @@ export interface PayloadLockedDocument { relationTo: 'skip-safe-fetch-media'; value: string | SkipSafeFetchMedia; } | null) + | ({ + relationTo: 'skip-safe-fetch-header-filter'; + value: string | SkipSafeFetchHeaderFilter; + } | null) | ({ relationTo: 'skip-allow-list-safe-fetch-media'; value: string | SkipAllowListSafeFetchMedia; @@ -2533,6 +2557,23 @@ export interface SkipSafeFetchMediaSelect { focalX?: T; focalY?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "skip-safe-fetch-header-filter_select". + */ +export interface SkipSafeFetchHeaderFilterSelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "skip-allow-list-safe-fetch-media_select". @@ -3409,6 +3450,6 @@ export interface Auth { declare module 'payload' { - // @ts-ignore + // @ts-ignore export interface GeneratedTypes extends Config {} -} \ No newline at end of file +} diff --git a/test/uploads/shared.ts b/test/uploads/shared.ts index d09ca0a4d19..701ab275977 100644 --- a/test/uploads/shared.ts +++ b/test/uploads/shared.ts @@ -30,6 +30,7 @@ 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 skipSafeFetchHeaderFilterSlug = 'skip-safe-fetch-header-filter' export const skipAllowListSafeFetchMediaSlug = 'skip-allow-list-safe-fetch-media' export const listViewPreviewSlug = 'list-view-preview' export const threeDimensionalSlug = 'three-dimensional'