mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 02:45:07 +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.
229 lines
8.1 KiB
TypeScript
229 lines
8.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
buildDatabaseEdgeFunctionUrl,
|
|
isEdgeFunctionUrl,
|
|
isValidEdgeFunctionURL,
|
|
} from './edgeFunctions'
|
|
|
|
describe('buildDatabaseEdgeFunctionUrl', () => {
|
|
it('builds a platform edge function URL', () => {
|
|
expect(
|
|
buildDatabaseEdgeFunctionUrl(
|
|
'hello-world',
|
|
'uniquetwentychararef',
|
|
'https://uniquetwentychararef.supabase.red/rest/v1/',
|
|
true
|
|
)
|
|
).toBe('https://uniquetwentychararef.supabase.red/functions/v1/hello-world')
|
|
})
|
|
|
|
it('builds a self-hosted URL reachable from Postgres', () => {
|
|
expect(
|
|
buildDatabaseEdgeFunctionUrl(
|
|
'hello-world',
|
|
'default',
|
|
'http://localhost:8000/rest/v1/',
|
|
false
|
|
)
|
|
).toBe('http://kong:8000/functions/v1/hello-world')
|
|
})
|
|
})
|
|
|
|
describe('isEdgeFunctionUrl', () => {
|
|
it('matches platform edge function URLs for the current project', () => {
|
|
expect(
|
|
isEdgeFunctionUrl(
|
|
'https://uniquetwentychararef.supabase.co/functions/v1/hello-world',
|
|
'uniquetwentychararef',
|
|
'https://uniquetwentychararef.supabase.co/rest/v1/',
|
|
true
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it('matches self-hosted edge function URLs', () => {
|
|
expect(
|
|
isEdgeFunctionUrl(
|
|
'http://kong:8000/functions/v1/hello-world',
|
|
'default',
|
|
'http://localhost:8000/rest/v1/',
|
|
false
|
|
)
|
|
).toBe(true)
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
name: 'a project URL without the functions path',
|
|
url: 'https://uniquetwentychararef.supabase.co/rest/v1/hello-world',
|
|
projectRef: 'uniquetwentychararef',
|
|
restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/',
|
|
isPlatform: true,
|
|
},
|
|
{
|
|
name: "another project's edge function URL",
|
|
url: 'https://anotherprojectref000.supabase.co/functions/v1/hello-world',
|
|
projectRef: 'uniquetwentychararef',
|
|
restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/',
|
|
isPlatform: true,
|
|
},
|
|
{
|
|
name: 'a lookalike project origin',
|
|
url: 'https://uniquetwentychararef.supabase.example.com/functions/v1/hello-world',
|
|
projectRef: 'uniquetwentychararef',
|
|
restUrl: 'https://uniquetwentychararef.supabase.co/rest/v1/',
|
|
isPlatform: true,
|
|
},
|
|
{
|
|
name: 'a self-hosted non-function URL',
|
|
url: 'http://kong:8000/rest/v1/hello-world',
|
|
projectRef: 'default',
|
|
restUrl: 'http://localhost:8000/rest/v1/',
|
|
isPlatform: false,
|
|
},
|
|
])('does not match $name', ({ url, projectRef, restUrl, isPlatform }) => {
|
|
expect(
|
|
isEdgeFunctionUrl(url, projectRef, restUrl, isPlatform),
|
|
`Expected ${url} not to match an edge function URL`
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isValidEdgeFunctionURL', () => {
|
|
const validEdgeFunctionUrls = [
|
|
'https://uniquetwentychararef.supabase.co/functions/v1/hello-world',
|
|
'https://uniquetwentychararef.supabase.red/functions/v1/hello-world',
|
|
'https://uniquetwentychararef.supabase.red/functions/v3/hello-world',
|
|
'https://uniquetwentychararef.supabase.red/functions/v3/hello-world',
|
|
]
|
|
|
|
const validLocalEdgeFunctionsUrls = [
|
|
'https://projectref.notsupabase.com/functions/v1/test',
|
|
'https://notsupabase.com/functions/v1/test',
|
|
'http://localhost:54321/functions/v1/test-2',
|
|
'http://kong:8000/functions/v1/hello-world',
|
|
'https://127.0.0.1:54321/functions/v1/test-3',
|
|
'https://127.0.0.1:54321/functions/v1/test-5',
|
|
]
|
|
|
|
const invalidPlatformEdgeFunctionUrls = [
|
|
'https://notsupabase.com/functions/v1/test',
|
|
'https://projectref.notsupabase.com/functions/v1/test',
|
|
'https://localhost?https://aaaa.supabase.co/functions/v1/xxx',
|
|
'https://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
|
|
'http://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
|
|
]
|
|
|
|
const invalidEdgeFunctionUrls = [
|
|
'https://localhost?https://aaaa.supabase.co/functions/v1/xxx',
|
|
'https://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
|
|
'http://localhost:3000/?https://aaaa.supabase.co/functions/v1/xxx',
|
|
]
|
|
|
|
it('should match valid edge function URLs on platform', () => {
|
|
for (const url of validEdgeFunctionUrls) {
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be valid`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should not match local URLs on platform', () => {
|
|
for (const url of validLocalEdgeFunctionsUrls) {
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid on platform`).toBe(
|
|
false
|
|
)
|
|
}
|
|
})
|
|
|
|
it('should match valid local edge function URLs off platform', () => {
|
|
for (const url of validLocalEdgeFunctionsUrls) {
|
|
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be valid`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('should not match invalid edge function URLs on platform', () => {
|
|
for (const url of invalidPlatformEdgeFunctionUrls) {
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid`).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('should not match invalid edge function URLs off platform', () => {
|
|
for (const url of invalidEdgeFunctionUrls) {
|
|
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be invalid`).toBe(false)
|
|
}
|
|
})
|
|
|
|
describe('with NIMBUS_PROD_PROJECTS_URL configured', () => {
|
|
const APEX = 'example-projects.com'
|
|
const REF = 'uniquetwentychararef'
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it.each([
|
|
{ name: 'the documented wildcard form', value: `https://*.${APEX}` },
|
|
{ name: 'a trailing slash', value: `https://*.${APEX}/` },
|
|
{ name: 'no wildcard prefix', value: `https://${APEX}` },
|
|
{ name: 'no scheme', value: `*.${APEX}` },
|
|
{ name: 'surrounding whitespace', value: ` https://*.${APEX} ` },
|
|
])('accepts a project URL when the env var is written with $name', ({ value }) => {
|
|
vi.stubEnv('NIMBUS_PROD_PROJECTS_URL', value)
|
|
|
|
expect(isValidEdgeFunctionURL(`https://${REF}.${APEX}/functions/v1/hello-world`, true)).toBe(
|
|
true
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
{ name: 'a functions subdomain', url: `https://${REF}.functions.${APEX}/functions/v1/hello` },
|
|
{ name: 'a ref containing digits', url: `https://ref1234567890abcdefg.${APEX}/functions/v1/x` },
|
|
{ name: 'an explicit port', url: `https://${REF}.${APEX}:8443/functions/v1/hello-world` },
|
|
{ name: 'a query string', url: `https://${REF}.${APEX}/functions/v1/hello-world?name=1` },
|
|
])('accepts $name', ({ url }) => {
|
|
vi.stubEnv('NIMBUS_PROD_PROJECTS_URL', `https://*.${APEX}`)
|
|
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be valid`).toBe(true)
|
|
})
|
|
|
|
it('still accepts default platform URLs', () => {
|
|
vi.stubEnv('NIMBUS_PROD_PROJECTS_URL', `https://*.${APEX}`)
|
|
|
|
for (const url of validEdgeFunctionUrls) {
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be valid`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('still accepts self-hosted URLs off platform', () => {
|
|
vi.stubEnv('NIMBUS_PROD_PROJECTS_URL', `https://*.${APEX}`)
|
|
|
|
for (const url of validLocalEdgeFunctionsUrls) {
|
|
expect(isValidEdgeFunctionURL(url, false), `Expected ${url} to be valid`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it.each([
|
|
{ name: 'an unrelated apex domain', url: 'https://someref.notexample.com/functions/v1/x' },
|
|
{ name: 'a lookalike apex domain', url: `https://someref.evil-${APEX}/functions/v1/x` },
|
|
{ name: 'the apex domain itself', url: `https://${APEX}/functions/v1/x` },
|
|
{ name: 'a non-functions path', url: `https://${REF}.${APEX}/rest/v1/x` },
|
|
{ name: 'plain http', url: `http://${REF}.${APEX}/functions/v1/x` },
|
|
])('rejects $name', ({ url }) => {
|
|
vi.stubEnv('NIMBUS_PROD_PROJECTS_URL', `https://*.${APEX}`)
|
|
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be invalid`).toBe(false)
|
|
})
|
|
|
|
it.each([{ value: '' }, { value: ' ' }])(
|
|
'falls back to the default hosts when the env var is blank ($value)',
|
|
({ value }) => {
|
|
vi.stubEnv('NIMBUS_PROD_PROJECTS_URL', value)
|
|
|
|
for (const url of validEdgeFunctionUrls) {
|
|
expect(isValidEdgeFunctionURL(url, true), `Expected ${url} to be valid`).toBe(true)
|
|
}
|
|
}
|
|
)
|
|
})
|
|
})
|