fix(10.1-04): refuse percent-encoded dot segments in plugin asset URLs

The URL parser resolves %2e%2e like .., so /{base}/assets/%2e%2e/api/...
passed assetAllowed and would load from outside the asset prefix. A
segment is now a dot segment after decoding %2e, in any case.

- tests/app/pluginAssets.test.ts covers the URL check, loadScript,
  loadStyles, activateStyles and loadControllerAssets
- modules/boardwalk/dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-29 02:52:14 +02:00
parent 7eed4acd87
commit 6b0ac15086
4 changed files with 211 additions and 4 deletions

View File

@@ -25,7 +25,9 @@ export function assetPrefix(): string {
/**
* Whether a URL may load as a plugin asset: a same-origin path under
* {base}/assets/ with no dot segment, backslash, whitespace or control
* character (browsers strip or rewrite those before resolving).
* character (browsers strip or rewrite those before resolving). A
* percent-encoded dot counts as a dot: the URL parser resolves %2e%2e like
* .., which would climb out of the asset prefix.
*/
export function assetAllowed(url: string): boolean {
if (!url.startsWith(assetPrefix()) || url.includes('\\')) {
@@ -38,7 +40,10 @@ export function assetAllowed(url: string): boolean {
}
}
const path = url.split(/[?#]/, 1)[0] ?? ''
return !path.split('/').some((segment) => segment === '.' || segment === '..')
return !path.split('/').some((segment) => {
const dots = segment.toLowerCase().replaceAll('%2e', '.')
return dots === '.' || dots === '..'
})
}
/** Loads one module script; the same URL always yields the same promise. */