fix: filters cookies with the payload- prefix in getExternalFile by default (#13215)

### What

- filters cookies with the `payload-` prefix in `getExternalFile` by
default (if `externalFileHeaderFilter` is not used).
- Document in `externalFileHeaderFilter`, that the user should handle
the removing of the payload cookie.

### Why

In the Payload application, the `getExternalFile` function sends the
user's cookies to an external server when fetching media, inadvertently
exposing the user's session to that third-party service.




```ts
const headers = uploadConfig.externalFileHeaderFilter
  ? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers)))
  : { cookie: req.headers?.get('cookie') };

const res = await fetch(fileURL, {
  credentials: 'include',
  headers,
  method: 'GET',
});
```
Although the
[externalFileHeaderFilter](https://payloadcms.com/docs/upload/overview#collection-upload-options)
function can strip sensitive cookies from the request, the default
config includes the session cookie, violating the secure-by-default
principle.

### How

- If `externalFileHeaderFilter` is not defined, any cookie beginning
with `payload-` is filtered.
- Added 2 tests: both for the case where `externalFileHeaderFilter` is
defined and for the case where it is not.





---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210561338171125
This commit is contained in:
German Jablonski
2025-07-29 21:21:50 +01:00
committed by GitHub
parent da8bf69054
commit 08942494e3
7 changed files with 158 additions and 31 deletions

View File

@@ -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

View File

@@ -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 =

View File

@@ -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

View File

@@ -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: [],

View File

@@ -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

View File

@@ -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<false> | MediaSelect<true>;
'allow-list-media': AllowListMediaSelect<false> | AllowListMediaSelect<true>;
'skip-safe-fetch-media': SkipSafeFetchMediaSelect<false> | SkipSafeFetchMediaSelect<true>;
'skip-safe-fetch-header-filter': SkipSafeFetchHeaderFilterSelect<false> | SkipSafeFetchHeaderFilterSelect<true>;
'skip-allow-list-safe-fetch-media': SkipAllowListSafeFetchMediaSelect<false> | SkipAllowListSafeFetchMediaSelect<true>;
'restrict-file-types': RestrictFileTypesSelect<false> | RestrictFileTypesSelect<true>;
'no-restrict-file-types': NoRestrictFileTypesSelect<false> | NoRestrictFileTypesSelect<true>;
@@ -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<T extends boolean = true> {
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<T extends boolean = true> {
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 {}
}
}

View File

@@ -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'