Skip to content

fix: escape regex metacharacters in isURLAllowed pathname allow-list#16997

Open
ciechanowiec wants to merge 1 commit into
payloadcms:mainfrom
ciechanowiec:fix/isURLAllowed-escape-regex
Open

fix: escape regex metacharacters in isURLAllowed pathname allow-list#16997
ciechanowiec wants to merge 1 commit into
payloadcms:mainfrom
ciechanowiec:fix/isURLAllowed-escape-regex

Conversation

@ciechanowiec

Copy link
Copy Markdown

What?

Escapes regex metacharacters in the pathname matching of isURLAllowed (the upload allow-list matcher) before compiling the pattern to a RegExp. Applied to both copies of the function (packages/payload and packages/ui) and adds a unit test.

Why?

isURLAllowed compiled an allow-list pathname pattern into a RegExp without escaping regex metacharacters, so a configured . matched any character — e.g. pattern /files/report.json also matched /files/reportXjson. The allow-list therefore accepted more URLs than configured.

This matters because isURLAllowed gates whether an upload-related fetch skips SSRF protection: in getFileFromURL.ts / getExternalFile.ts an allow-list match (pasteURL.allowList / skipSafeFetch) uses a plain fetch instead of safeFetch. Over-broad pathname matching widens that boundary. The hostname field is required and matched exactly, so this is not a full cross-host SSRF bypass — it is a defense-in-depth / least-privilege weakening on an already-trusted (often internal) host.

Separately, ** was translated to .* before * was translated to [^/]*, so the * inside the freshly inserted .* was rewritten again: /uploads/** compiled to /uploads/.[^/]* and failed to match /uploads/nested/file.png (fail-closed correctness bug).

How?

Escape the pattern first via the existing escapeRegExp utility (the same approach wordBoundariesRegex.ts already uses), then restore the now-escaped \*\* / \* wildcards. Escaping first also resolves the ordering bug, because the resulting .* no longer contains an escaped \* for the next replace to match:

const regexPattern = escapeRegExp(value)
  .replace(/\\\*\\\*/g, '.*') // `**` → match any path
  .replace(/\\\*/g, '[^/]*') // `*`  → match any part of a path segment
  .replace(/\/$/, '(/)?') // Allow optional trailing slash

Adds packages/payload/src/utilities/isURLAllowed.spec.ts covering literal metacharacters, * vs ** segment semantics, exact hostname matching (incl. the userinfo@host case), and the optional trailing slash.

Fixes #16996

The upload allow-list matcher (isURLAllowed) compiled a pathname pattern
into a RegExp without escaping regex metacharacters, so a configured '.'
matched any character and the allow-list accepted more URLs than intended.
'**' was also rewritten before '*', corrupting '**' into '.[^/]*'.

Escape the pattern first (via the existing escapeRegExp utility), then
restore the now-escaped wildcards. Applied to both the payload and ui
copies. Adds a co-located unit test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isURLAllowed does not escape regex metacharacters in pathname patterns, widening upload allow-list matching

1 participant