fix: (regression 3.0.2) some illegal paths were not blocked

Massimo Melina committed Apr 1, 2026 at 16:37 UTC fe02718e946219ddccac0a57b99a5b0e76dd6435
3 files changed +13 -2
src/cross.ts
+3 -1
@@ -464,7 +464,9 @@ export async function promiseBestEffort<T>(promises: Promise<T>[]) {
464 export function pathEncode(s: string, all=false) {
465 return all ? encodeURI(s).replace(/#/g, escape) : s.replace(/[:&#'"% ?\\]/g, escape) // escape() is not utf8, but we are encoding only ascii chars
466 }
467 -export function pathDecode(s: string) { return decodeURI(s).replace(/%23/g, '#') }
467 +export function pathDecode(s: string) {
468 + return decodeURI(s).replace(/%23/g, '#')
469 +}
470
471 // run at a specific point in time, also solving the limit of setTimeout, which doesn't work with +32bit delays
472 export function runAt(ts: number, cb: Callback) {
src/middlewares.ts
+4 -1
@@ -3,7 +3,7 @@
3 import compress from 'koa-compress'
4 import Koa from 'koa'
5 import { API_URI, DEV } from './const'
6 -import { ALLOW_SESSION_IP_CHANGE, DAY, isLocalHost, netMatches, splitAt, stream2string, tryJson } from './misc'
6 +import { ALLOW_SESSION_IP_CHANGE, DAY, hasDirTraversal, isLocalHost, netMatches, splitAt, stream2string, try_, tryJson } from './misc'
7 import { Readable } from 'stream'
8 import { applyBlock } from './block'
9 import { Account, accountCanLogin, getAccount, getFromAccount } from './perm'
@@ -62,6 +62,9 @@ export const someSecurity: Koa.Middleware = (ctx, next) => {
62
63 if (!ctx.state.skipFilters && applyBlock(ctx.socket, ctx.ip))
64 return
65 + const decodedPath = try_(() => decodeURI(ctx.path))
66 + if (!decodedPath || hasDirTraversal(decodedPath))
67 + return
68
69 if (ctx.get('X-Forwarded-For')
70 // we have some dev-proxies to ignore
tests/test.ts
+6
@@ -1026,6 +1026,12 @@ describe('admin', () => {
1026 return entry.hits || 0
1027 }
1028 })
1029 + test('plugins.public traversal', async () => {
1030 + const id = 'list-uploader'
1031 + await reqApi('start_plugin', { id }, 200, { auth })()
1032 + return req(`/~/plugins/${id}/../../../tests/config.yaml`, 404)()
1033 + .finally(() => reqApi('stop_plugin', { id }, 200, { auth })())
1034 + })
1035 })
1036
1037 function login(usr: string, pwd=password) {