mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
`isValidEdgeFunctionURL` guards the server-side fetch in
`pages/api/edge-functions/test.ts`. When `NIMBUS_PROD_PROJECTS_URL` is set it
built a regex by string concatenation, which had several problems:
- The branch returned early, so a deployment configured with an additional
projects domain rejected every `*.supabase.co` / `*.supabase.red` URL, and
rejected self-hosted URLs even when `isPlatform` was false.
- The guard was `!== undefined`, so an env var declared-but-blank took the
branch and rejected every URL.
- The subdomain pattern `[a-z]*` matched a single label of letters only, so
refs containing digits, nested subdomains, and explicit ports all failed.
- Only `.` was escaped before interpolation into the regex, so a value without
the exact `https://*.` prefix or with a trailing slash silently produced a
pattern that matched nothing.
- Reading `process.env` at module scope made the branch untestable, and it had
no test coverage.
Parse the URL instead of pattern-matching it, and treat the additional domain
as additive to the default hosts rather than a replacement.
The default `[a-z]{20}.supabase.(co|red)` host check is deliberately unchanged.
This function doubles as an SSRF allowlist for a fetch that carries the
caller's credentials, so the default allowlist is left exactly as it was.
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import { IS_PLATFORM } from '@/lib/constants'
|
|
|
|
// Cron jobs and database hooks run inside Postgres, where Kong is available by this network alias.
|
|
const SELF_HOSTED_EDGE_FUNCTIONS_URL = 'http://kong:8000/functions/v1'
|
|
const PLATFORM_TLDS = ['co', 'red'] as const
|
|
|
|
export const buildDatabaseEdgeFunctionUrl = (
|
|
slug: string,
|
|
projectRef: string,
|
|
restUrl?: string,
|
|
isPlatform = IS_PLATFORM
|
|
) => {
|
|
if (!isPlatform) return `${SELF_HOSTED_EDGE_FUNCTIONS_URL}/${slug}`
|
|
|
|
const projectOrigin = restUrl ? new URL(restUrl).origin : `https://${projectRef}.supabase.co`
|
|
return `${projectOrigin}/functions/v1/${slug}`
|
|
}
|
|
|
|
export const isEdgeFunctionUrl = (
|
|
url: string,
|
|
projectRef: string,
|
|
restUrl?: string,
|
|
isPlatform = IS_PLATFORM
|
|
) => {
|
|
if (!isPlatform && url.startsWith(`${SELF_HOSTED_EDGE_FUNCTIONS_URL}/`)) return true
|
|
|
|
const projectOrigin = restUrl ? new URL(restUrl).origin : undefined
|
|
if (projectOrigin && url.startsWith(`${projectOrigin}/functions/v1/`)) return true
|
|
|
|
return PLATFORM_TLDS.some(
|
|
(tld) =>
|
|
url.startsWith(`https://${projectRef}.functions.supabase.${tld}/`) ||
|
|
url.startsWith(`https://${projectRef}.supabase.${tld}/functions/`)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Normalises `NIMBUS_PROD_PROJECTS_URL` (e.g. `https://*.example.com`) down to its apex domain.
|
|
* Returns null when unset, blank, or whitespace-only so callers fall through to the default hosts.
|
|
* Read at call time rather than module scope so the value is stubbable in tests.
|
|
*/
|
|
const getAdditionalProjectsApexDomain = () => {
|
|
const configured = process.env.NIMBUS_PROD_PROJECTS_URL?.trim()
|
|
if (!configured) return null
|
|
|
|
return (
|
|
configured
|
|
.replace(/^https?:\/\//, '')
|
|
.replace(/^\*\./, '')
|
|
.replace(/\/+$/, '')
|
|
.toLowerCase() || null
|
|
)
|
|
}
|
|
|
|
const isFunctionsPath = (pathname: string) => /^\/functions\/v\d\/.+/.test(pathname)
|
|
|
|
/**
|
|
* Guards the server-side fetch in `pages/api/edge-functions/test.ts`, so this doubles as an
|
|
* SSRF allowlist: only hosts that match are fetched with the caller's credentials attached.
|
|
*/
|
|
export const isValidEdgeFunctionURL = (url: string, isPlatform: boolean) => {
|
|
let parsed: URL
|
|
try {
|
|
parsed = new URL(url)
|
|
} catch {
|
|
return false
|
|
}
|
|
|
|
// Parse rather than pattern-match the whole URL, so credentials/query smuggling such as
|
|
// `https://localhost?https://ref.supabase.co/functions/v1/x` can't satisfy the host check.
|
|
if (!isFunctionsPath(parsed.pathname)) return false
|
|
|
|
if (!isPlatform) return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
|
|
|
if (parsed.protocol !== 'https:') return false
|
|
|
|
const host = parsed.hostname.toLowerCase()
|
|
|
|
// Additive: a deployment serving additional project domains must still validate the
|
|
// default ones, so this is checked alongside PLATFORM_TLDS rather than instead of them.
|
|
const additionalApexDomain = getAdditionalProjectsApexDomain()
|
|
if (additionalApexDomain && host.endsWith(`.${additionalApexDomain}`)) return true
|
|
|
|
return PLATFORM_TLDS.some((tld) => new RegExp(`^[a-z]{20}\\.supabase\\.${tld}$`).test(host))
|
|
}
|