payloadcms/payload · error · APIError

Invalid filename.

Error message

Invalid filename.

What it means

APIError (HTTP 400) thrown as a path-traversal guard in the local-filesystem fallback of getFileHandler. After resolving staticDir and joining the requested filename, if the resolved path does not start with resolvedDir + path.sep, the filename tried to escape the upload directory (e.g. via '..' segments or an absolute path).

Source

Thrown at packages/payload/src/uploads/endpoints/getFile.ts:73

      })
      if (customResponse && customResponse instanceof Response) {
        break
      }
    }

    if (customResponse instanceof Response) {
      return customResponse
    }
  }

  // Local filesystem fallback — cloud storage handlers return a Response above
  // and have their own filename validation via sanitizeFilename.
  const fileDir = collection.config.upload?.staticDir || collection.config.slug
  const resolvedDir = path.resolve(fileDir)
  const filePath = path.resolve(resolvedDir, filename)

  if (!filePath.startsWith(resolvedDir + path.sep)) {
    throw new APIError('Invalid filename.', httpStatus.BAD_REQUEST)
  }

  let stats: Stats

  try {
    stats = await fsPromises.stat(filePath)
  } catch (err) {
    if ((err as { code?: string }).code === 'ENOENT') {
      req.payload.logger.error(
        `File ${filename} for collection ${collection.config.slug} is missing on the disk. Expected path: ${filePath}`,
      )

      // Omit going to the routeError handler by returning response instead of
      // throwing an error to cut down log noise. The response still matches what you get with APIError to not leak details to the user.
      return Response.json(
        {
          errors: [
            {

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Sanitize filenames on the client/request layer (drop '/', '\', '..') before constructing the URL.
  2. Ensure cloud-storage upload handlers return a Response so execution never reaches the filesystem traversal branch.
  3. Confirm staticDir is a stable absolute base with no symlinks escaping it.
  4. If serving custom filenames, route them through sanitizeFilename (used elsewhere in Payload) before file lookups.

Example fix

// client — before
const url = `/api/media/file/${userInput}`
// after — strip path separators and traversal
const safeName = userInput.replace(/[/\\]|\.\./g, '')
const url = `/api/media/file/${encodeURIComponent(safeName)}`
Defensive patterns

Strategy: validation

Validate before calling

function isSafeFilename(name: string): boolean {
  // reject traversal, separators, absolute, empty
  return /^[^/\\\u0000]+$/.test(name) && !name.includes('..') && name.trim().length > 0
}
if (!isSafeFilename(filename)) throw new Error('Invalid filename')

Type guard

const isSanitizedFilename = (name: string): boolean =>
  !/[\/\\]|\.\./.test(name) && name.length > 0 && !name.includes('\u0000')

Try / catch

try {
  await fetch(`/api/media/file/${encodeURIComponent(name)}`)
} catch (e) {
  if (/Invalid filename/.test((e as Error).message)) alert('Bad filename')
}

Prevention

When it happens

Trigger: A GET /api/:collection/file/<filename> request where <filename> contains '../' sequences, a leading slash making it resolve outside staticDir, or NUL/encoded traversal that resolves outside the upload dir. Triggered only on the local-filesystem path (after cloud handlers return nothing).

Common situations: Malicious or buggy client constructing filenames from user input; URL-encoding tricks (%2e%2e%2f); symlink inside staticDir pointing outside; misconfigured staticDir with a trailing component that defeats the startsWith check; a custom upload handler that didn't return a Response letting execution fall through to the filesystem branch.

Related errors


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