fix: get=list was not complying with configured roots

Massimo Melina committed May 14, 2026 at 19:33 UTC 135b66c0f8fcdd2b07381bfc330c4c56b1591fa8
4 files changed +30 -5
src/roots.ts
+2 -2
@@ -30,7 +30,7 @@ export const rootsMiddleware: Koa.Middleware = (ctx, next) =>
30 if (referer && try_(() => new URL(referer).pathname.startsWith(ctx.state.revProxyPath + ADMIN_URI))) return // exclude apis for admin-panel
31 }
32 if (_.isEmpty(roots.get())) return
33 - const root = ctx.state.root = roots.compiled()?.(ctx.host)
33 + const root = ctx.state.root = roots.compiled()(ctx.host)
34 if (!ctx.state.skipFilters && forceAddress.get()
35 && root === undefined && !isLocalHost(ctx) && ctx.host !== baseUrl.compiled())
36 return disconnect(ctx, forceAddress.key()) // returning truthy will not call next
@@ -52,4 +52,4 @@ declare module "koa" {
52 originalPath: string // before roots is applied
53 root?: string
54 }
55 -}
\ No newline at end of file
55 +}
src/serveGuiAndSharedFiles.ts
+13 -3
@@ -23,6 +23,7 @@ import {
23 asyncGeneratorToReadable, filterMapGenerator, isValidFileName, loadFileCached, pathEncode, safeDecodeURIComponent,
24 try_, pathDecodeSegments,
25 } from './misc'
26 +import { roots } from './roots'
27 import XXH from 'xxhashjs'
28 import fs from 'fs'
29 import { rm } from 'fs/promises'
@@ -160,10 +161,19 @@ async function sendFolderList(node: VfsNode, ctx: Koa.Context) {
161 ctx.type = 'text'
162 if (prepend === undefined || prepend === '*') { // * = force auto-detection even if we have baseUrl set
163 const { URL } = ctx
163 - const base = prepend === undefined && baseUrl.get()
164 - || URL.protocol + '//' + URL.host + ctx.state.revProxyPath
164 + const requestBase = URL.protocol + '//' + URL.host + ctx.state.revProxyPath
165 + const configuredBaseUrl = prepend === undefined && baseUrl.get()
166 + const configuredRoot = configuredBaseUrl && roots.compiled()(baseUrl.compiled() || '')
167 + const pathInConfiguredRoot = configuredRoot && (configuredRoot === '/' ? ctx.path
168 + : ctx.path.startsWith(configuredRoot) ? ctx.path.slice(configuredRoot.length - 1)
169 + : ctx.path === configuredRoot.slice(0, -1) ? '/'
170 + : false)
171 + // base_url may expose a host-rooted home; use it only for VFS paths inside that host root
172 + const [base, path] = !configuredBaseUrl ? [requestBase, ctx.path]
173 + : pathInConfiguredRoot === false ? [requestBase, ctx.path]
174 + : [configuredBaseUrl, pathInConfiguredRoot || ctx.path]
175 // redo the encoding our way, keeping unicode chars unchanged. decode each segment separately because decodeURI preserves reserved escapes like %3A, which pathEncode would double-encode
166 - prepend = base + pathDecodeSegments(ctx.path, pathEncode)
176 + prepend = base + pathDecodeSegments(path, pathEncode)
177 }
178 const walker = walkNode(node, { ctx, depth: depth === '*' ? Infinity : Number(depth), parallelizeRecursion: false }) // parallelization produces out-of-order results, and we don't want it like that here
179 ctx.body = asyncGeneratorToReadable(filterMapGenerator(walker, async el => {
tests/config.yaml
+1
@@ -1,4 +1,5 @@
1 port: 8081
2 +base_url: http://127.0.0.1:8081
3 open_browser_at_start: false
4 allowed_referer: x.com
5 localhost_admin: false
tests/test.ts
+14
@@ -154,6 +154,20 @@ describe('basics', () => {
154 if (String(data).includes('/tests/C%253A/'))
155 throw Error('double encoded path in list: ' + data)
156 }))
157 + test('folder list strips base_url root', req('/f1/f2/?get=list&folders=*', data => {
158 + data = String(data)
159 + if (!data.includes(`${BASE_URL_127}/f2/alfa.txt`))
160 + throw Error('missing base_url-rooted path in list: ' + data)
161 + if (data.includes(`${BASE_URL_127}/f1/f2/alfa.txt`))
162 + throw Error('base_url root still present in list: ' + data)
163 + }))
164 + test('folder list ignores base_url outside its root', req('/tests/?get=list&folders=*', data => {
165 + data = String(data)
166 + if (!data.includes(`${BASE_URL}/tests/page/`))
167 + throw Error('missing request-host path in list: ' + data)
168 + if (data.includes(BASE_URL_127))
169 + throw Error('base_url used outside its root: ' + data)
170 + }))
171
172 test('missing perm', reqList('/for-admins/', 401))
173 test('missing perm.file', req('/for-admins/alfa.txt', 401))