better input sanitization

Massimo Melina committed Feb 6, 2022 at 23:17 UTC 81c106054b5a646230d318b29c56ffb9bb0095e1
4 files changed +22 -18
src/api.vfs.ts
+1 -1
@@ -18,7 +18,7 @@ function saveVfs() {
18 const apis: ApiHandlers = {
19
20 async get_vfs() {
21 - return { root:vfs.root && await recur(vfs.root) }
21 + return { root: vfs.root && await recur(vfs.root) }
22
23 async function recur(n: VfsNode): Promise<VfsAdmin> {
24 const dir = await nodeIsDirectory(n)
src/index.ts
+3 -1
@@ -7,7 +7,7 @@ import { log } from './log'
7 import { pluginsMiddleware } from './plugins'
8 import { throttler } from './throttler'
9 import { getAccount, getCurrentUsername, getCurrentUsernameExpanded } from './perm'
10 -import { headRequests, gzipper, sessions, frontendAndSharedFiles } from './middlewares'
10 +import { headRequests, gzipper, sessions, frontendAndSharedFiles, someSecurity } from './middlewares'
11 import './listen'
12 import { serveAdminFiles } from './serveFrontend'
13 import { adminApis } from './adminApis'
@@ -20,11 +20,13 @@ console.log('started', HFS_STARTED.toLocaleString(), 'build', BUILD_TIMESTAMP, D
20 console.debug('cwd', process.cwd())
21
22 export const adminApp = new Koa()
23 + .use(someSecurity)
24 .use(mount(API_URI, apiMiddleware(adminApis)))
25 .use(serveAdminFiles)
26 .on('error', errorHandler)
27
28 export const app = new Koa({ keys: ['hfs-keys-test'] })
29 +app.use(someSecurity)
30 app.use(sessions(app))
31 app.use(async (ctx, next) => {
32 ctx.state.usernames = getCurrentUsernameExpanded(ctx) // accounts chained via .belongs for permissions check
src/middlewares.ts
+7 -15
@@ -8,7 +8,7 @@ import { vfs } from './vfs'
8 import { isDirectory } from './misc'
9 import { zipStreamFromFolder } from './zip'
10 import { serveFileNode } from './serveFile'
11 -import { serveAdminFiles, serveFrontend } from './serveFrontend'
11 +import { serveFrontend } from './serveFrontend'
12 import mount from 'koa-mount'
13 import { Readable } from 'stream'
14
@@ -25,7 +25,7 @@ export const gzipper = compress({
25 export const headRequests: Koa.Middleware = async (ctx, next) => {
26 const head = ctx.method === 'HEAD'
27 if (head)
28 - ctx.method = 'GET' // let's other middleware work so we can collect the size at the end
28 + ctx.method = 'GET' // let other middlewares work, so we can collect the size at the end
29 await next()
30 if (!head || ctx.body === undefined) return
31 const { length, status } = ctx.response
@@ -48,14 +48,11 @@ const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontend)
48
49 export const frontendAndSharedFiles: Koa.Middleware = async (ctx, next) => {
50 const { path } = ctx
51 - if (path.includes('..'))
52 - ctx.throw(500)
51 if (ctx.body)
52 return next()
53 if (path.startsWith(FRONTEND_URI))
54 return serveFrontendPrefixed(ctx,next)
57 - const decoded = decodeURI(path)
58 - const node = await vfs.urlToNode(decoded, ctx)
55 + const node = await vfs.urlToNode(path, ctx)
56 if (!node)
57 return next()
58 const { source } = node
@@ -68,7 +65,7 @@ export const frontendAndSharedFiles: Koa.Middleware = async (ctx, next) => {
65 if (!path.endsWith('/')) // this folder was requested without the trailing /
66 return ctx.redirect(path + '/')
67 if (node.default) {
71 - const def = await vfs.urlToNode(decoded + node.default, ctx)
68 + const def = await vfs.urlToNode(path + node.default, ctx)
69 if (def)
70 return serveFileNode(def)(ctx, next)
71 }
@@ -80,13 +77,8 @@ export const frontendAndSharedFiles: Koa.Middleware = async (ctx, next) => {
77 return next()
78 }
79
83 -export const admin: Koa.Middleware = async (ctx, next) => {
84 - const { path } = ctx
85 - if (path.includes('..'))
86 - ctx.throw(500)
87 - if (ctx.body)
88 - return next()
89 - if (path === '/')
90 - serveAdminFiles(ctx, next)
80 +export const someSecurity: Koa.Middleware = async (ctx, next) => {
81 + if (decodeURI(ctx.path).includes('..'))
82 + return ctx.status = 418
83 return next()
84 }
src/vfs.ts
+11 -1
@@ -42,7 +42,13 @@ export class Vfs {
42
43 async urlToNode(url: string, ctx?: Koa.Context, root?: VfsNode) : Promise<VfsNode | undefined> {
44 let run = root || this.root
45 - const rest = url.split('/').filter(Boolean).map(decodeURIComponent)
45 + const decoded = decodeURI(url)
46 + if (decoded.includes('..')) {
47 + if (ctx)
48 + ctx.status = 418
49 + return
50 + }
51 + const rest = decoded.split('/').filter(Boolean)
52 if (ctx && !hasPermission(run, ctx)) return
53 while (rest.length) {
54 let piece = rest.shift() as string
@@ -55,6 +61,10 @@ export class Vfs {
61 if (!run.source)
62 return
63 const relativeSource = piece + prefix('/', rest.join('/'))
64 + if (relativeSource.includes('..')) {
65 + ctx?.throw(418)
66 + return
67 + }
68 const baseSource = run.source+ '/'
69 const source = baseSource + relativeSource
70 if (run.remove && isMatch(source, run.remove.split('|').map(x => baseSource + x)))