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:
@@ -91,7 +91,7 @@ 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. |
|
||||
@@ -99,7 +99,7 @@ _An asterisk denotes that an option is required._
|
||||
| **`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. |
|
||||
| **`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) |
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user