Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/lib/edge-functions/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,13 +499,24 @@ export class EdgeFunctionsRegistryImpl implements EdgeFunctionsRegistry {
matchURLPath(urlPath: string, method: string, headers: Record<string, string | string[] | undefined>) {
const functionNames: string[] = []
const routeIndexes: number[] = []
// `urlPath` comes from `URL.prototype.pathname`, which does not decode
// percent-encoding. Route patterns are written against decoded paths
// (e.g. `/admin/*`), so a request to `/%61dmin/x` would otherwise skip a
// route meant to match `/admin/x`. Malformed sequences fall back to the
// raw path rather than throwing.
let decodedUrlPath = urlPath
try {
decodedUrlPath = decodeURIComponent(urlPath)
} catch {
// Leave decodedUrlPath as the raw urlPath.
}

this.routes.forEach((route, index) => {
if (route.methods && route.methods.length !== 0 && !route.methods.includes(method)) {
return
}

if (!route.pattern.test(urlPath)) {
if (!route.pattern.test(decodedUrlPath)) {
return
}

Expand Down Expand Up @@ -539,13 +550,13 @@ export class EdgeFunctionsRegistryImpl implements EdgeFunctionsRegistry {
}

const isExcludedForFunction = this.manifest?.function_config[route.function]?.excluded_patterns?.some((pattern) =>
new RegExp(pattern).test(urlPath),
new RegExp(pattern).test(decodedUrlPath),
)
if (isExcludedForFunction) {
return
}

const isExcludedForRoute = route.excluded_patterns.some((pattern) => new RegExp(pattern).test(urlPath))
const isExcludedForRoute = route.excluded_patterns.some((pattern) => new RegExp(pattern).test(decodedUrlPath))
if (isExcludedForRoute) {
return
}
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/lib/edge-functions/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,49 @@ describe('EdgeFunctionsRegistryImpl.build() coalescing', () => {
expect(state.buildCount).toBe(2)
})
})

describe('EdgeFunctionsRegistryImpl.matchURLPath() percent-encoded paths', () => {
const createRegistryWithRoute = () => {
const registry = Object.create(EdgeFunctionsRegistryImpl.prototype) as EdgeFunctionsRegistryImpl
// `routes` and `manifest` are private; matchURLPath() only reads them, so
// setting them directly is enough to exercise it in isolation.
const registryInternals = registry as unknown as { routes: unknown[]; manifest: unknown }

// A route pattern as it would be compiled for `/admin/*`: written against
// the decoded path, same as it's authored in a project's routing config.
registryInternals.routes = [
{
function: 'admin-guard',
pattern: /^\/admin\/.*$/,
excluded_patterns: [],
},
]
registryInternals.manifest = null

return registry
}

test('matches a route for the literal, unencoded path', () => {
const registry = createRegistryWithRoute()

const { functionNames } = registry.matchURLPath('/admin/secretpath', 'GET', {})

expect(functionNames).toEqual(['admin-guard'])
})

test('matches a route when the request path is percent-encoded', () => {
const registry = createRegistryWithRoute()

// "%61" is a percent-encoded "a": this requests the same logical path as
// /admin/secretpath, just spelled differently on the wire.
const { functionNames } = registry.matchURLPath('/%61dmin/secretpath', 'GET', {})

expect(functionNames).toEqual(['admin-guard'])
})

test('falls back to the raw path for malformed percent-encoding instead of throwing', () => {
const registry = createRegistryWithRoute()

expect(() => registry.matchURLPath('/admin/%', 'GET', {})).not.toThrow()
})
})