payloadcms/payload · critical · Forbidden

You are not allowed to perform this action.

Error message

You are not allowed to perform this action.

What it means

During duplication or re-upload (`shouldReupload`), Payload reconstructs the file from the existing document's `filename`/`url`. If that `filename` contains a path-traversal sequence — either `../` (Unix) or `..\\` (Windows backslash) — Payload throws `Forbidden` (HTTP 403). This blocks an attacker from crafting a document whose stored filename escapes `staticDir` on the next write (`getFileByPath(`${staticPath}/${filename}`)` would otherwise resolve outside the upload folder).

Source

Thrown at packages/payload/src/uploads/generateFileData.ts:122

    imageSizes,
    resizeOptions,
    staticDir,
    trimOptions,
    withMetadata,
  } = collectionConfig.upload

  const staticPath = staticDir

  const incomingFileData: Document = isDuplicating ? originalDoc : data
  let isLocalFile = false

  if (
    !file &&
    (isDuplicating || shouldReupload(uploadEdits, incomingFileData as Record<string, unknown>))
  ) {
    const { filename, url } = incomingFileData as unknown as FileData
    if (filename && (filename.includes('../') || filename.includes('..\\'))) {
      throw new Forbidden(req.t)
    }

    if ((serverURL && url?.startsWith(serverURL)) || url?.startsWith('/')) {
      isLocalFile = true
    }

    try {
      if (!disableLocalStorage && isLocalFile) {
        // File is stored locally
        const filePath = `${staticPath}/${filename}`
        const response = await getFileByPath(filePath)
        file = response
        overwriteExistingFiles = true
      } else if (filename && url) {
        // File is remote
        file = await getExternalFile({
          data: incomingFileData as unknown as FileData,
          req,

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Audit and sanitize stored `filename` values in the database (remove `..` segments); the normal write path uses `sanitize-filename` so any non-conforming value was written out-of-band.
  2. Trace where the offending `filename` originated (import script, custom hook, direct DB write) and fix the writer to use `sanitize-filename` / `getSafeFileName`.
  3. If the document is meant to reference an external file, use the `url` path (validated by `isURLAllowed`) rather than embedding traversal in `filename`.
  4. Add a migration to rewrite existing bad rows before re-uploading.
  5. Reject such filenames at ingest with a `beforeChange` hook that validates against `/\.\.[\\/]/`.
  6. For local file refetch, prefer resolving via the collection's storage metadata instead of trusting the stored filename string.

Example fix

// before — migration wrote a bad filename
{ filename: '../../etc/passwd', url: '/media/../../etc/passwd' }

// after — sanitize on write
import sanitize from 'sanitize-filename'
await payload.update({
  collection: 'media',
  id,
  data: { filename: sanitize(badFilename) },
})
Defensive patterns

Strategy: validation

Validate before calling

function isSafeFilename(name: string): boolean {
  return !name.includes('../') && !name.includes('..\\')
}

for (const doc of await payload.db.find({ collection: 'media', where: {} })) {
  if (doc.filename && !isSafeFilename(doc.filename)) {
    // flag for repair — sanitize and rewrite
  }
}

Type guard

function hasNoTraversal(name: unknown): name is string {
  return typeof name === 'string' && !name.includes('../') && !name.includes('..\\')
}

if (!hasNoTraversal(doc.filename)) {
  throw new Error(`Stored filename contains a traversal sequence: ${doc.filename}`)
}

Try / catch

// Guard at ingest with a beforeChange hook
beforeChange: [({ data }) => {
  if (typeof data.filename === 'string' && (data.filename.includes('../') || data.filename.includes('..\\'))) {
    throw new Error('Refusing to store a filename with a path-traversal sequence')
  }
}]

Prevention

When it happens

Trigger: A create/update/duplicate operation on an upload collection where `req.file` is absent, `isDuplicating` is true OR `shouldReupload(uploadEdits, incomingFileData)` is true, and `incomingFileData.filename` (the persisted filename on the document) contains `../` or `..\\`. This typically means a bad/stored filename in the database.

Common situations: A migration imported documents with un-sanitized filenames. A custom hook or external writer stored a relative path in the `filename` field. A plugin-cloud-storage prefix accidentally landed in `filename`. Manual DB edits left traversal sequences in place.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/5867b8646d5d101b. Report an issue: GitHub.