feat: validate field names for reserved names (#7130)

We now validate the names of the field against an array of protected
field names.

Also added JSDoc since we can't enforce type strictness yet if `string |
const[]` as it always evaluates to `string`.

```
The name of the field. Must be alphanumeric and cannot contain ' . '

Must not be one of protected field names: ['__v', 'salt', 'hash', 'file']

@link — [https://payloadcms.com/docs/fields/overview#field-names](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)
```
This commit is contained in:
Paul
2024-07-12 16:27:10 -04:00
committed by GitHub
parent c359c34ee8
commit 03d854ed18
8 changed files with 69 additions and 21 deletions

View File

@@ -143,6 +143,7 @@ The following field names are forbidden and cannot be used:
- `__v`
- `salt`
- `hash`
- `file`
### Field-level Hooks

View File

@@ -36,6 +36,18 @@ export const sanitizeCollection = async (
isMergeableObject: isPlainObject,
})
// /////////////////////////////////
// Sanitize fields
// /////////////////////////////////
const validRelationships = config.collections.map((c) => c.slug) || []
sanitized.fields = await sanitizeFields({
config,
fields: sanitized.fields,
richTextSanitizationPromises,
validRelationships,
})
if (sanitized.timestamps !== false) {
// add default timestamps fields only as needed
let hasUpdatedAt = null
@@ -162,17 +174,5 @@ export const sanitizeCollection = async (
sanitized.fields = mergeBaseFields(sanitized.fields, authFields)
}
// /////////////////////////////////
// Sanitize fields
// /////////////////////////////////
const validRelationships = config.collections.map((c) => c.slug) || []
sanitized.fields = await sanitizeFields({
config,
fields: sanitized.fields,
richTextSanitizationPromises,
validRelationships,
})
return sanitized as SanitizedCollectionConfig
}

View File

@@ -0,0 +1,9 @@
import type { FieldAffectingData } from '../fields/config/types.js'
import { APIError } from './APIError.js'
export class ReservedFieldName extends APIError {
constructor(field: FieldAffectingData, fieldName: string) {
super(`Field ${field.label} has reserved name '${fieldName}'.`)
}
}

View File

@@ -18,4 +18,5 @@ export { MissingFieldType } from './MissingFieldType.js'
export { MissingFile } from './MissingFile.js'
export { NotFound } from './NotFound.js'
export { QueryError } from './QueryError.js'
export { ReservedFieldName } from './ReservedFieldName.js'
export { ValidationError } from './ValidationError.js'

View File

@@ -9,7 +9,12 @@ import type {
TextField,
} from './types.js'
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
import {
InvalidFieldName,
InvalidFieldRelationship,
MissingFieldType,
ReservedFieldName,
} from '../../errors/index.js'
import { sanitizeFields } from './sanitize.js'
describe('sanitizeFields', () => {
@@ -47,6 +52,23 @@ describe('sanitizeFields', () => {
}).rejects.toThrow(InvalidFieldName)
})
it('should throw on a reserved field name', async () => {
const fields: Field[] = [
{
name: 'hash',
type: 'text',
label: 'hash',
},
]
await expect(async () => {
await sanitizeFields({
config,
fields,
validRelationships: [],
})
}).rejects.toThrow(ReservedFieldName)
})
describe('auto-labeling', () => {
it('should populate label if missing', async () => {
const fields: Field[] = [

View File

@@ -7,6 +7,7 @@ import {
InvalidFieldName,
InvalidFieldRelationship,
MissingFieldType,
ReservedFieldName,
} from '../../errors/index.js'
import { deepMerge } from '../../utilities/deepMerge.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
@@ -39,6 +40,8 @@ type Args = {
validRelationships: null | string[]
}
export const reservedFieldNames = ['__v', 'salt', 'hash', 'file']
export const sanitizeFields = async ({
config,
existingFieldNames = new Set(),
@@ -59,6 +62,11 @@ export const sanitizeFields = async ({
throw new InvalidFieldName(field, field.name)
}
// assert that field names are not one of reserved names
if (fieldAffectsData(field) && reservedFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
// Auto-label
if (
'name' in field &&

View File

@@ -229,6 +229,12 @@ export interface FieldBase {
index?: boolean
label?: LabelFunction | Record<string, string> | false | string
localized?: boolean
/**
* The name of the field. Must be alphanumeric and cannot contain ' . '
*
* Must not be one of reserved field names: ['__v', 'salt', 'hash', 'file']
* @link https://payloadcms.com/docs/fields/overview#field-names
*/
name: string
required?: boolean
saveToJWT?: boolean | string

View File

@@ -41,6 +41,15 @@ export const sanitizeGlobals = async (
if (!global.hooks.beforeRead) global.hooks.beforeRead = []
if (!global.hooks.afterRead) global.hooks.afterRead = []
// Sanitize fields
const validRelationships = collections.map((c) => c.slug) || []
global.fields = await sanitizeFields({
config,
fields: global.fields,
richTextSanitizationPromises,
validRelationships,
})
if (global.versions) {
if (global.versions === true) global.versions = { drafts: false }
@@ -103,14 +112,6 @@ export const sanitizeGlobals = async (
})
}
const validRelationships = collections.map((c) => c.slug) || []
global.fields = await sanitizeFields({
config,
fields: global.fields,
richTextSanitizationPromises,
validRelationships,
})
globals[i] = global
}