/** * Full project audit regression harness. * * Starts the backend with an isolated database/storage root and exercises the * highest-risk public HTTP flows through real routes, cookies and CSRF. */ const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); const http = require('http'); const os = require('os'); const path = require('path'); const { spawn } = require('child_process'); const BACKEND_DIR = path.resolve(__dirname, '..'); const SERVER_PATH = path.join(BACKEND_DIR, 'server.js'); const AUDIT_PREFIX = `audit_${Date.now()}_${crypto.randomBytes(3).toString('hex')}`; function randomHex(bytes = 32) { return crypto.randomBytes(bytes).toString('hex'); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function countRegularFiles(dir) { if (!fs.existsSync(dir)) return 0; let count = 0; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.isFile()) count += 1; } return count; } class CookieJar { constructor() { this.cookies = new Map(); } store(setCookie) { const list = Array.isArray(setCookie) ? setCookie : (setCookie ? [setCookie] : []); for (const raw of list) { const first = String(raw).split(';')[0]; const idx = first.indexOf('='); if (idx <= 0) continue; this.cookies.set(first.slice(0, idx), first.slice(idx + 1)); } } header() { return Array.from(this.cookies.entries()) .map(([key, value]) => `${key}=${value}`) .join('; '); } get(name) { return this.cookies.get(name) || ''; } } async function request(baseUrl, jar, method, route, options = {}) { const url = new URL(route, baseUrl); const headers = { ...(options.headers || {}) }; const cookieHeader = jar?.header(); if (cookieHeader) headers.Cookie = cookieHeader; const upperMethod = method.toUpperCase(); if (!['GET', 'HEAD', 'OPTIONS'].includes(upperMethod) && options.csrf !== false) { const csrf = jar?.get('csrf_token'); if (csrf) headers['X-CSRF-Token'] = csrf; } let body = options.body; if (options.json !== undefined) { headers['Content-Type'] = 'application/json'; body = JSON.stringify(options.json); } const response = await fetch(url, { method: upperMethod, headers, body, redirect: options.redirect || 'manual' }); jar?.store(response.headers.getSetCookie ? response.headers.getSetCookie() : response.headers.get('set-cookie')); const contentType = response.headers.get('content-type') || ''; const buffer = Buffer.from(await response.arrayBuffer()); let data = buffer; if (contentType.includes('application/json')) { data = JSON.parse(buffer.toString('utf8') || '{}'); } else if (contentType.includes('text/') || contentType.includes('image/svg')) { data = buffer.toString('utf8'); } return { status: response.status, headers: response.headers, data, raw: buffer }; } async function waitForHealth(baseUrl, timeoutMs = 15000) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { try { const res = await request(baseUrl, null, 'GET', '/api/health'); if (res.status === 200 && res.data?.success === true) return; } catch {} await delay(250); } throw new Error('server did not become healthy'); } function getFreePort() { return new Promise((resolve, reject) => { const server = http.createServer(); server.listen(0, '127.0.0.1', () => { const address = server.address(); server.close(() => resolve(address.port)); }); server.on('error', reject); }); } async function run() { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wanwanyun-full-audit-')); const storageRoot = path.join(tempRoot, 'storage'); const dbPath = path.join(tempRoot, 'database.db'); const port = await getFreePort(); const baseUrl = `http://127.0.0.1:${port}`; const uploadsDir = path.join(BACKEND_DIR, 'uploads'); const adminPassword = `${AUDIT_PREFIX}_Pass123!`; const env = { ...process.env, PORT: String(port), NODE_ENV: 'development', DATABASE_PATH: dbPath, STORAGE_ROOT: storageRoot, JWT_SECRET: randomHex(32), ENCRYPTION_KEY: randomHex(32), ADMIN_USERNAME: 'admin', ADMIN_PASSWORD: adminPassword, PUBLIC_BASE_URL: baseUrl, ALLOWED_ORIGINS: baseUrl, COOKIE_SECURE: 'false', ENABLE_CSRF: 'true', ENFORCE_HTTPS: 'false', TRUST_PROXY: 'false', WAL_CHECKPOINT_ENABLED: 'false' }; const child = spawn(process.execPath, [SERVER_PATH], { cwd: BACKEND_DIR, env, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', chunk => { stdout += chunk.toString(); }); child.stderr.on('data', chunk => { stderr += chunk.toString(); }); const tests = []; const test = (name, fn) => tests.push({ name, fn }); try { await waitForHealth(baseUrl); const jar = new CookieJar(); let userId = 1; let shareId = null; let shareCode = ''; let directLinkId = null; test('public health/config/csrf endpoints are reachable', async () => { const health = await request(baseUrl, jar, 'GET', '/api/health'); assert.strictEqual(health.status, 200); assert.strictEqual(health.data.success, true); const config = await request(baseUrl, jar, 'GET', '/api/config'); assert.strictEqual(config.status, 200); assert.strictEqual(config.data.success, true); const csrf = await request(baseUrl, jar, 'GET', '/api/csrf-token'); assert.strictEqual(csrf.status, 200); assert.ok(csrf.data.csrfToken); assert.ok(jar.get('csrf_token')); }); test('auth endpoints login with real cookies and enforce CSRF after authentication', async () => { const login = await request(baseUrl, jar, 'POST', '/api/login', { json: { username: 'admin', password: adminPassword } }); assert.strictEqual(login.status, 200); assert.strictEqual(login.data.success, true); assert.ok(jar.get('token')); assert.ok(jar.get('refreshToken')); userId = login.data.user.id; const profile = await request(baseUrl, jar, 'GET', '/api/user/profile'); assert.strictEqual(profile.status, 200); assert.strictEqual(profile.data.success, true); assert.strictEqual(profile.data.user.id, userId); assert.strictEqual(profile.data.user.oss_access_key_secret, undefined); const csrfBlocked = await request(baseUrl, jar, 'POST', '/api/user/theme', { csrf: false, json: { theme: 'light' } }); assert.strictEqual(csrfBlocked.status, 403); }); test('admin can move isolated audit user to local storage only', async () => { const res = await request(baseUrl, jar, 'POST', `/api/admin/users/${userId}/storage-permission`, { json: { storage_permission: 'local_only', local_storage_quota: 10 * 1024 * 1024, download_traffic_quota: -1, reset_download_traffic_used: true } }); assert.strictEqual(res.status, 200); assert.strictEqual(res.data.success, true); assert.strictEqual(res.data.user.current_storage_type, 'local'); }); test('file manager rejects unsafe folder names and handles normal local file flow', async () => { const badMkdir = await request(baseUrl, jar, 'POST', '/api/files/mkdir', { json: { path: '/', folderName: '../bad' } }); assert.strictEqual(badMkdir.status, 400); const badList = await request(baseUrl, jar, 'GET', '/api/files?path=/../secret'); assert.strictEqual(badList.status, 400); const mkdir = await request(baseUrl, jar, 'POST', '/api/files/mkdir', { json: { path: '/', folderName: AUDIT_PREFIX } }); assert.strictEqual(mkdir.status, 200); assert.strictEqual(mkdir.data.success, true); const file = new Blob([`hello ${AUDIT_PREFIX}`], { type: 'text/plain' }); const form = new FormData(); form.append('path', `/${AUDIT_PREFIX}`); form.append('file', file, 'hello.txt'); const upload = await request(baseUrl, jar, 'POST', '/api/upload', { body: form }); assert.strictEqual(upload.status, 200); assert.strictEqual(upload.data.success, true); assert.strictEqual(upload.data.path, `/${AUDIT_PREFIX}/hello.txt`); const list = await request(baseUrl, jar, 'GET', `/api/files?path=/${encodeURIComponent(AUDIT_PREFIX)}`); assert.strictEqual(list.status, 200); assert.ok(list.data.items.some(item => item.name === 'hello.txt')); const search = await request(baseUrl, jar, 'GET', `/api/files/search?keyword=hello&path=/${encodeURIComponent(AUDIT_PREFIX)}`); assert.strictEqual(search.status, 200); assert.ok(search.data.items.some(item => item.name === 'hello.txt')); }); test('failed normal upload validation must not leave multer temp files', async () => { fs.mkdirSync(uploadsDir, { recursive: true }); const before = countRegularFiles(uploadsDir); const form = new FormData(); form.append('path', '/../blocked'); form.append('file', new Blob(['leak candidate'], { type: 'text/plain' }), 'leak.txt'); const res = await request(baseUrl, jar, 'POST', '/api/upload', { body: form }); assert.strictEqual(res.status, 400); const after = countRegularFiles(uploadsDir); assert.strictEqual(after, before, `uploads temp leak: before=${before}, after=${after}`); }); test('download URL/check/download work for local file and reject traversal', async () => { const traversal = await request(baseUrl, jar, 'GET', '/api/files/download-check?path=/../secret.txt'); assert.strictEqual(traversal.status, 400); const check = await request(baseUrl, jar, 'GET', `/api/files/download-check?path=/${encodeURIComponent(AUDIT_PREFIX)}/hello.txt`); assert.strictEqual(check.status, 200); assert.strictEqual(check.data.success, true); const url = await request(baseUrl, jar, 'GET', `/api/files/download-url?path=/${encodeURIComponent(AUDIT_PREFIX)}/hello.txt&mode=download`); assert.strictEqual(url.status, 400); assert.match(url.data.message, /OSS/); const download = await request(baseUrl, jar, 'GET', `/api/files/download?path=/${encodeURIComponent(AUDIT_PREFIX)}/hello.txt`); assert.strictEqual(download.status, 200); assert.ok(download.raw.toString('utf8').includes(AUDIT_PREFIX)); }); test('share and direct-link flows preserve path boundaries', async () => { const createShare = await request(baseUrl, jar, 'POST', '/api/share/create', { json: { share_type: 'directory', file_path: `/${AUDIT_PREFIX}`, file_name: AUDIT_PREFIX, password: `${AUDIT_PREFIX}_pw`, expiry_days: 1, max_downloads: 5, device_limit: 'all' } }); assert.strictEqual(createShare.status, 200); assert.strictEqual(createShare.data.success, true); shareId = createShare.data.share_id; shareCode = createShare.data.share_code; const badVerify = await request(baseUrl, new CookieJar(), 'POST', `/api/share/${shareCode}/verify`, { json: { password: 'wrong' } }); assert.strictEqual(badVerify.status, 401); const publicJar = new CookieJar(); const verify = await request(baseUrl, publicJar, 'POST', `/api/share/${shareCode}/verify`, { json: { password: `${AUDIT_PREFIX}_pw` } }); assert.strictEqual(verify.status, 200); assert.strictEqual(verify.data.success, true); const list = await request(baseUrl, publicJar, 'POST', `/api/share/${shareCode}/list`, { json: { path: '', password: `${AUDIT_PREFIX}_pw` } }); assert.strictEqual(list.status, 200); assert.strictEqual(list.data.success, true); assert.ok(list.data.items.some(item => item.name === 'hello.txt')); const traversal = await request(baseUrl, publicJar, 'POST', `/api/share/${shareCode}/download-url`, { json: { path: '/../database.db', mode: 'download', password: `${AUDIT_PREFIX}_pw` } }); assert.ok([400, 403, 404].includes(traversal.status)); const direct = await request(baseUrl, jar, 'POST', '/api/direct-link/create', { json: { file_path: `/${AUDIT_PREFIX}/hello.txt`, file_name: 'hello.txt', expiry_days: 1 } }); assert.strictEqual(direct.status, 200); assert.strictEqual(direct.data.success, true); directLinkId = direct.data.link_id; }); test('admin listing/logging endpoints are authenticated and sanitized', async () => { const noAuth = await request(baseUrl, new CookieJar(), 'GET', '/api/admin/users'); assert.strictEqual(noAuth.status, 401); const users = await request(baseUrl, jar, 'GET', '/api/admin/users?page=1&pageSize=10'); assert.strictEqual(users.status, 200); assert.strictEqual(users.data.success, true); const adminRow = users.data.users.find(user => user.id === userId); assert.ok(adminRow); assert.strictEqual(adminRow.password, undefined); assert.strictEqual(adminRow.oss_access_key_secret, undefined); const logs = await request(baseUrl, jar, 'GET', '/api/admin/logs?page=1&pageSize=5'); assert.strictEqual(logs.status, 200); assert.strictEqual(logs.data.success, true); }); test('cleanup via public APIs succeeds for audit artifacts', async () => { if (directLinkId) { const res = await request(baseUrl, jar, 'DELETE', `/api/direct-link/${directLinkId}`); assert.ok([200, 404].includes(res.status)); } if (shareId) { const res = await request(baseUrl, jar, 'DELETE', `/api/share/${shareId}`); assert.ok([200, 404].includes(res.status)); } const del = await request(baseUrl, jar, 'POST', '/api/files/delete', { json: { path: '/', fileName: AUDIT_PREFIX } }); assert.strictEqual(del.status, 200); assert.strictEqual(del.data.success, true); }); const failures = []; for (const item of tests) { try { await item.fn(); console.log(`[PASS] ${item.name}`); } catch (error) { failures.push({ name: item.name, error }); console.error(`[FAIL] ${item.name}`); console.error(error.stack || error.message); } } if (failures.length > 0) { const summary = failures.map(item => `- ${item.name}: ${item.error.message}`).join('\n'); throw new Error(`full audit regression failed:\n${summary}`); } console.log(`PASS full-audit-regression (${tests.length} tests)`); } catch (error) { console.error('--- backend stdout tail ---'); console.error(stdout.split(/\r?\n/).slice(-80).join('\n')); console.error('--- backend stderr tail ---'); console.error(stderr.split(/\r?\n/).slice(-80).join('\n')); throw error; } finally { child.kill('SIGTERM'); await delay(500); if (!child.killed) child.kill('SIGKILL'); fs.rmSync(tempRoot, { recursive: true, force: true }); } } run().catch(error => { console.error(error.stack || error.message); process.exit(1); });