#!/usr/bin/env node import { randomBytes, scryptSync } from "node:crypto"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); function hashPassword(password) { const normalized = password.normalize("NFKC"); const salt = randomBytes(16).toString("hex"); const hash = scryptSync(normalized, `boss:${salt}`, 64).toString("hex"); return `scrypt$${salt}$${hash}`; } function nowIso() { return new Date().toISOString(); } function projectTemplate(id, name, deviceId) { const timestamp = nowIso(); return { id, name, pinned: false, deviceIds: [deviceId], preview: "", updatedAt: timestamp, lastMessageAt: timestamp, isGroup: false, threadMeta: { projectId: id, threadId: `thread-${id}`, threadDisplayName: name, folderName: name, activityIconCount: 1, updatedAt: timestamp, }, groupMembers: [], createdByAgent: false, collaborationMode: "development", approvalState: "not_required", unreadCount: 0, riskLevel: "low", messages: [], goals: [], versions: [], }; } async function waitForServer(baseUrl, child, getServerLogs) { for (let attempt = 0; attempt < 60; attempt += 1) { if (child.exitCode !== null) { throw new Error(`SERVER_EXITED_EARLY:${child.exitCode}:${getServerLogs()}`); } try { const response = await fetch(`${baseUrl}/api/health`); if (response.ok) { return; } } catch {} await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error(`SERVER_START_TIMEOUT:${getServerLogs()}`); } async function login(baseUrl, account, password) { const response = await fetch(`${baseUrl}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify({ account, password, method: "password", }), }); if (!response.ok) { throw new Error(`LOGIN_FAILED:${account}:${response.status}`); } const cookie = (response.headers.get("set-cookie") || "").split(";")[0]; if (!cookie) { throw new Error(`COOKIE_MISSING:${account}`); } return cookie; } const tmpRoot = await mkdtemp(path.join(os.tmpdir(), "boss-attachment-security-")); const runtimeRoot = path.join(tmpRoot, "runtime"); const dataDir = path.join(runtimeRoot, "data"); const uploadsDir = path.join(dataDir, "uploads"); const stateFile = path.join(dataDir, "boss-state.json"); const outsideSecretPath = path.join(runtimeRoot, "secret-outside-uploads.txt"); const port = "3104"; const baseUrl = `http://127.0.0.1:${port}`; let server; try { await mkdir(path.join(runtimeRoot, "public", "downloads"), { recursive: true }); await mkdir(uploadsDir, { recursive: true }); const baseState = JSON.parse(await readFile(path.join(repoRoot, "data", "boss-state.json"), "utf8")); const timestamp = nowIso(); const memberAccount = "18800000001"; const memberPassword = "member-pass-123"; const memberDeviceId = "member-device-1"; const memberProject = projectTemplate("member-project", "成员项目", memberDeviceId); const adminProject = projectTemplate("admin-only-project", "管理员项目", "mac-studio"); const adminAttachmentPath = path.join("data", "uploads", "seeded-admin", "2026", "03", "admin-note.txt"); const adminAbsolutePath = path.join(runtimeRoot, adminAttachmentPath); await mkdir(path.dirname(adminAbsolutePath), { recursive: true }); await writeFile(adminAbsolutePath, "admin only attachment\n", "utf8"); const traversalAttachmentId = "att-traversal"; const adminAttachmentId = "att-admin-only"; await writeFile(outsideSecretPath, "outside uploads secret\n", "utf8"); memberProject.messages.push({ id: "msg-traversal", sender: "user", senderLabel: "成员用户", body: "traversal probe", sentAt: timestamp, kind: "attachment", attachments: [ { attachmentId: traversalAttachmentId, fileName: "secret.txt", mimeType: "text/plain", fileSizeBytes: 23, attachmentKind: "text", storageBackend: "server_file", storagePath: outsideSecretPath, previewAvailable: false, uploadedAt: timestamp, uploadedBy: memberAccount, analysisState: "ready_manual", }, ], }); memberProject.preview = "traversal probe"; adminProject.messages.push({ id: "msg-admin-attachment", sender: "user", senderLabel: "Boss 超级管理员", body: "管理员附件", sentAt: timestamp, kind: "attachment", attachments: [ { attachmentId: adminAttachmentId, fileName: "admin-note.txt", mimeType: "text/plain", fileSizeBytes: 22, attachmentKind: "text", storageBackend: "server_file", storagePath: adminAttachmentPath, previewAvailable: false, uploadedAt: timestamp, uploadedBy: "krisolo", analysisState: "ready_manual", }, ], }); adminProject.preview = "管理员附件"; baseState.authSessions = []; baseState.authAccounts.push({ id: `account-${memberAccount}`, account: memberAccount, passwordHash: hashPassword(memberPassword), displayName: "成员用户", role: "member", primaryDeviceId: memberDeviceId, createdAt: timestamp, updatedAt: timestamp, }); baseState.devices.push({ id: memberDeviceId, name: "Member Device", avatar: "M", account: memberAccount, source: "production", status: "online", projects: [memberProject.id], quota5h: 10, quota7d: 20, lastSeenAt: timestamp, }); baseState.projects.push(memberProject, adminProject); baseState.userAttachmentStorageConfigs = [ ...(Array.isArray(baseState.userAttachmentStorageConfigs) ? baseState.userAttachmentStorageConfigs : []), { account: memberAccount, mode: "server_file", updatedAt: timestamp, }, ]; await writeFile(stateFile, JSON.stringify(baseState, null, 2), "utf8"); server = spawn("node", [".next/standalone/server.js"], { cwd: repoRoot, env: { ...process.env, PORT: port, HOSTNAME: "127.0.0.1", BOSS_RUNTIME_ROOT: runtimeRoot, BOSS_STATE_FILE: stateFile, BOSS_AUTH_AUTO_LOGIN: "0", }, stdio: ["ignore", "pipe", "pipe"], }); let serverLogs = ""; server.stdout.on("data", (chunk) => { serverLogs += chunk.toString(); }); server.stderr.on("data", (chunk) => { serverLogs += chunk.toString(); }); await waitForServer(baseUrl, server, () => serverLogs); const memberCookie = await login(baseUrl, memberAccount, memberPassword); const adminCookie = await login(baseUrl, "krisolo", "Admin_yqs_asd."); const uploadForm = new FormData(); uploadForm.append("file", new File([Buffer.from("blocked upload\n")], "blocked.txt", { type: "text/plain" })); const uploadDenied = await fetch(`${baseUrl}/api/v1/projects/admin-only-project/attachments`, { method: "POST", headers: { cookie: memberCookie, }, body: uploadForm, }); if (uploadDenied.status !== 403) { throw new Error(`EXPECTED_UPLOAD_403:${uploadDenied.status}`); } const adminDownload = await fetch(`${baseUrl}/api/v1/attachments/${adminAttachmentId}/download`, { headers: { cookie: memberCookie, }, }); if (adminDownload.status !== 403) { throw new Error(`EXPECTED_DOWNLOAD_403:${adminDownload.status}`); } const happyAdminDownload = await fetch(`${baseUrl}/api/v1/attachments/${adminAttachmentId}/download`, { headers: { cookie: adminCookie, }, }); if (!happyAdminDownload.ok) { throw new Error(`ADMIN_DOWNLOAD_FAILED:${happyAdminDownload.status}`); } const traversalDownload = await fetch(`${baseUrl}/api/v1/attachments/${traversalAttachmentId}/download`, { headers: { cookie: memberCookie, }, }); if (traversalDownload.status !== 404) { throw new Error(`EXPECTED_TRAVERSAL_404:${traversalDownload.status}`); } console.log("OK"); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; } finally { if (server && server.exitCode === null) { server.kill("SIGTERM"); await new Promise((resolve) => server.once("exit", resolve)); } await rm(tmpRoot, { recursive: true, force: true }); }