fix: prevent delete/rename of virtual nodes
Massimo Melina committed
May 8, 2025 at 18:16 UTC
2f729c421866094641b3b89a75707ff21072e5e2
5 files changed
+19
-21
src/api.get_file_list.ts
+1
-1
@@ -47,7 +47,7 @@ export const get_file_list: ApiHandler = async ({ uri='/', offset, limit, c, onl
47
const walker = walkNode(node, { ctx: admin ? undefined : ctx, onlyFolders, onlyFiles, depth })
48
const onDirEntryHandlers = mapPlugins(plug => plug.onDirEntry)
49
const can_upload = admin || hasPermission(node, 'can_upload', ctx)
50
- const fakeChild = await applyParentToChild({ source: 'dummy-file' }, node) // used to check permission; simple but but can produce false results
50
+ const fakeChild = await applyParentToChild({ source: 'dummy-file', original: undefined }, node) // used to check permission; simple but can produce false results; 'original' to simulate a non-vfs node
51
const can_delete = admin || hasPermission(fakeChild, 'can_delete', ctx)
52
const can_archive = admin || hasPermission(fakeChild, 'can_archive', ctx)
53
const can_comment = can_upload && areCommentsEnabled()
src/api.vfs.ts
+1
-1
@@ -280,7 +280,7 @@ export function pickProps(o: any, keys: string[]) {
280
return ret
281
}
282
283
-function simplifyName(node: VfsNode) {
283
+export function simplifyName(node: VfsNode) {
284
const { name, ...noName } = node
285
if (getNodeName(noName) === name)
286
delete node.name
src/frontEndApis.ts
+11
-14
@@ -11,8 +11,9 @@ import {
11
HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED
12
} from './const'
13
import {
14
- hasPermission, isRoot, nodeIsDirectory, nodeStats, statusCodeForMissingPerm, urlToNode, VfsNode, walkNode
14
+ hasPermission, isRoot, nodeIsDirectory, nodeStats, saveVfs, statusCodeForMissingPerm, urlToNode, VfsNode, walkNode
15
} from './vfs'
16
+import { simplifyName } from './api.vfs'
17
import fs from 'fs'
18
import { mkdir, rename, copyFile, unlink } from 'fs/promises'
19
import { basename, dirname, join } from 'path'
@@ -91,19 +92,15 @@ export const frontEndApis: ApiHandlers = {
92
if (!hasPermission(node, 'can_delete', ctx))
93
throw new ApiError(HTTP_UNAUTHORIZED)
94
try {
94
- if (node.name) // virtual name = virtual rename
95
- node.name = dest
96
- else {
97
- if (!node.source)
98
- throw new ApiError(HTTP_FAILED_DEPENDENCY)
99
- const destSource = join(dirname(node.source), dest)
100
- await rename(node.source, destSource)
101
- getCommentFor(node.source).then(c => {
102
- if (!c) return
103
- void setCommentFor(node.source!, '')
104
- void setCommentFor(destSource, c)
105
- })
106
- }
95
+ if (!node.source)
96
+ throw new ApiError(HTTP_FAILED_DEPENDENCY)
97
+ const destSource = join(dirname(node.source), dest)
98
+ await rename(node.source, destSource)
99
+ getCommentFor(node.source).then(c => {
100
+ if (!c) return
101
+ void setCommentFor(node.source!, '')
102
+ void setCommentFor(destSource, c)
103
+ })
104
return {}
105
}
106
catch (e: any) {
src/vfs.ts
+5
-4
@@ -88,7 +88,7 @@ function normalizeFilename(x: string) {
88
89
export async function applyParentToChild(child: VfsNode | undefined, parent: VfsNode, name?: string) {
90
const ret: VfsNode = {
91
- original: child, // leave it possible for child to override this
91
+ original: child, // this can be overridden by passing an 'original' in `child`
92
...child,
93
isFolder: child?.isFolder ?? (child?.children?.length! > 0 || undefined), // isFolder is hidden in original node, so we must read it to copy it
94
isTemp: true,
@@ -164,7 +164,7 @@ export async function getNodeByName(name: string, parent: VfsNode) {
164
}
165
if (!isValidFileName(onDisk)) return
166
ret.source = join(parent.source, onDisk)
167
- ret.original = undefined // overwrite in applyParentToChild, so we know this is not part of the vfs
167
+ ret.original = undefined // this will overwrite the 'original' set in applyParentToChild, so we know this is not part of the vfs
168
return ret
169
}
170
}
@@ -247,7 +247,8 @@ export function statusCodeForMissingPerm(node: VfsNode, perm: keyof VfsPerms, ct
247
return ret
248
249
function getCode() {
250
- if (!node.source && (perm === 'can_upload' || perm === 'can_delete')) // Upload possible only if we know where to store. First check node.source because is supposedly faster.
250
+ if ((isRoot(node) || node.original) && perm === 'can_delete' // we currently don't allow deleting of vfs nodes from frontend
251
+ || !node.source && perm === 'can_upload') // Upload possible only if we know where to store. First check node.source because is supposedly faster.
252
return HTTP_FORBIDDEN
253
// calculate value of permission resolving references to other permissions, avoiding infinite loop
254
let who: Who | undefined
@@ -308,7 +309,7 @@ export async function* walkNode(parent: VfsNode, {
309
const nodeName = getNodeName(child)
310
const name = prefixPath + nodeName
311
taken?.add(normalizeFilename(name))
311
- const item = { ...child, name }
312
+ const item = { ...child, original: child, name }
313
if (await cantSee(item)) continue
314
if (item.source && !item.children?.length) // real items must be accessible, unless there's more to it
315
try { await fs.access(item.source) }
tests/test.ts
+1
-1
@@ -143,7 +143,7 @@ describe('basics', () => {
143
}))
144
test('create_folder', reqApi('create_folder', { uri: UPLOAD_ROOT, name: 'temp' }, 401))
145
test('delete.no perm', req('/for-admins/', 405, { method: 'delete' }))
146
- test('delete.need account', req(UPLOAD_ROOT, 401, { method: 'delete'}))
146
+ test('delete.need account', req(UPLOAD_ROOT + 'alfa.txt', 401, { method: 'delete'}))
147
test('rename.no perm', reqApi('rename', { uri: '/for-admins', dest: 'any' }, 401))
148
test('of_disabled.cantLogin', () => login('of_disabled').then(() => { throw Error('logged in') }, () => {}))
149
test('allow_net.canLogin', () => login(username))