@samitouri / QOSami-HFS / commits / d1325ad7

fix: forbid upload-temp-hash without permission

Massimo Melina committed Jan 15, 2026 at 23:27 UTC d1325ad7c394ee08d9c2dd1bcbb8b1f6db8a2353
3 files changed +12 -5
package.json
+2 -2
@@ -13,7 +13,7 @@
13 "watch-server-full": "npm run start --workspace=frontend & npm run start --workspace=admin & cross-env FRONTEND_PROXY=3005 ADMIN_PROXY=3006 npm run watch-server",
14 "start-frontend": "npm run start --workspace=frontend",
15 "start-admin": "npm run start --workspace=admin",
16 - "build-all": "npm audit --omit=dev --audit-level=moderate && rm -rf dist && npm i && npm run build-server && npm run test-with-server && (npm run build-frontend & npm run build-admin) && echo COMPLETED",
16 + "build-all": "rm -rf dist && npm run build-server && npm run test-with-server && (npm run build-frontend & npm run build-admin) && echo COMPLETED",
17 "build-server": "rm -rf dist/src dist/plugins && npm i && tsc && touch package.json && cp -v -r package.json central.json README* LICENSE* hfs.ico plugins dist && find dist -name .DS_Store -o -name storage -exec rm -rf {} + && node afterbuild.js",
18 "build-frontend": "npm run build --workspace=frontend",
19 "build-admin": "npm run build --workspace=admin",
@@ -25,7 +25,7 @@
25 "test-with-ui": "npx playwright test --ui",
26 "pub": "cd dist && npm publish",
27 "dist": "STASHED=; if ! git diff-index --quiet HEAD --; then git stash push -m 'dist' && STASHED=1; fi; CI=1 FORCE_COLOR=1 npm run dist-uncommitted || (EXIT_CODE=$?; [ -n \"$STASHED\" ] && git stash pop; exit $EXIT_CODE); [ -n \"$STASHED\" ] && git stash pop",
28 - "dist-uncommitted": "npm run build-all && npm run test-ui && npm run dist-bin",
28 + "dist-uncommitted": "npm audit --omit=dev --audit-level=moderate && npm run build-all && npm run test-ui && npm run dist-bin",
29 "dist-bin": "npm run dist-modules && npm run dist-bin-win && npm run dist-bin-linux && npm run dist-bin-mac && npm run dist-bin-mac-arm",
30 "dist-modules": "cp package*.json central.json dist && cd dist && npm ci --omit=dev && cd .. && node prune_modules",
31 "dist-bin-win": "cd dist && pkg . --public -C gzip -t node20-win-x64 && npx resedit-cli --in hfs.exe --icon 1,../hfs.ico --out hfs.exe && zip hfs-windows-x64-$(jq -r .version ../package.json).zip hfs.exe -r plugins && cd ..",
src/serveGuiAndSharedFiles.ts
+2 -1
@@ -68,7 +68,8 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
68 ctx.state.uploadPath = decPath
69 if (getUploadTempHash)
70 return !folder.source ? sendErrorPage(ctx, HTTP_NOT_FOUND)
71 - : ctx.body = await loadFileCached(getUploadTempFor(join(folder.source, rest)), calcHash) // negligible memory leak
71 + : statusCodeForMissingPerm(folder, 'can_upload', ctx) ? null
72 + : ctx.body = await loadFileCached(getUploadTempFor(join(folder.source, rest)), calcHash) // negligible memory leak
73 const dest = uploadWriter(folder, folderUri, rest, ctx)
74 if (dest) {
75 ctx.req.pipe(dest).on('error', err => {
tests/test.ts
+8 -2
@@ -5,7 +5,7 @@ import { createReadStream, existsSync, statfsSync, statSync } from 'fs'
5 import { basename, dirname, resolve } from 'path'
6 import { exec } from 'child_process'
7 import _ from 'lodash'
8 -import { findDefined, randomId, try_, tryJson, wait } from '../src/cross'
8 +import { findDefined, randomId, try_, tryJson, UPLOAD_TEMP_HASH, wait } from '../src/cross'
9 import { httpStream, stream2string, XRequestOptions } from '../src/util-http'
10 import { ThrottledStream, ThrottleGroup } from '../src/ThrottledStream'
11 import { mkdir, rm, writeFile } from 'fs/promises'
@@ -26,7 +26,8 @@ const ROOT = 'tests/'
26 const BASE_URL = 'http://[::1]:81'
27 const BASE_URL_127 = 'http://127.0.0.1:81'
28 const UPLOAD_ROOT = '/for-admins/upload/'
29 -const UPLOAD_RELATIVE = 'temp/gpl.png'
29 +const UPLOAD_DIR = 'temp'
30 +const UPLOAD_RELATIVE = `${UPLOAD_DIR}/gpl.png`
31 const UPLOAD_DEST = UPLOAD_ROOT + UPLOAD_RELATIVE
32 const BIG_CONTENT = _.repeat(randomId(10), 300_000) // 3MB, big enough to saturate buffers
33 const throttle = BIG_CONTENT.length /1000 /0.8 // KB, finish in 0.8s, quick but still overlapping downloads
@@ -245,6 +246,11 @@ describe('after-login', () => {
246 test('inherit.disabled', reqList('/for-disabled/', 401))
247 test('upload.never', reqUpload('/random', 403))
248 test('upload.ok', reqUpload(UPLOAD_DEST, 200))
249 + test('upload.temp hash requires auth', async () => {
250 + const rel = `${UPLOAD_DIR}/partial.png`
251 + await reqUpload(`${UPLOAD_ROOT}${rel}?partial=1`, 204)()
252 + await req(`${UPLOAD_ROOT}${rel}?get=${UPLOAD_TEMP_HASH}`, 401, { jar: {} })()
253 + })
254 test('file_details.admin', reqApi('get_file_details', { uris: [UPLOAD_DEST] }, res => {
255 const u = res?.details?.[0]?.upload
256 throwIf(!u?.ip ? 'ip' : u?.username !== username ? 'username' : '')