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)))