文件
websafe-kb/00-environments/templates/fixtures/shared/node_fixture.mjs

172 行
6.3 KiB
JavaScript

import fs from "node:fs";
import http from "node:http";
const scenario = JSON.parse(fs.readFileSync(process.env.LAB_FIXTURE_SCENARIO, "utf8"));
const port = Number(process.env.PORT || 3000);
const state = {
seeded: false,
proof: false,
family: scenario.family,
system_id: scenario.system_id,
case_id: "",
detail: "fixture ready",
uploads: [],
sink_hits: 0,
payload: null,
events: []
};
function note(event, detail) {
state.events.push({ event, detail });
state.events = state.events.slice(-20);
}
function sendJson(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
res.end(body);
}
function renderHtml() {
const proof = state.proof;
const banner = proof ? `<div class="proof">Proof active: ${state.detail}</div>` : `<div class="baseline">Baseline ready</div>`;
const xssBlock = proof && state.family === "xss"
? `<script>document.documentElement.setAttribute("data-xss-proof","true");document.title=${JSON.stringify(`${scenario.title} - proof`)};</script><div id="xss-proof">XSS marker executed for ${state.case_id}</div>`
: "";
const uploads = state.uploads.length ? `<section><h2>Uploads</h2><ul>${state.uploads.map((item) => `<li>${item.filename}</li>`).join("")}</ul></section>` : "";
const sink = state.sink_hits ? `<section id="ssrf-proof">Local sink hits: ${state.sink_hits}</section>` : "";
const admin = proof && ["proxy-boundary", "authz-bypass"].includes(state.family)
? `<section id="admin-proof">Admin boundary bypass confirmed.</section>`
: "";
const deserialize = proof && state.family === "deserialization"
? `<section id="deserialize-proof">Decoded marker: ${state.case_id}</section>`
: "";
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${scenario.title}${proof && state.family !== "xss" ? " - proof" : ""}</title>
<style>
body { font-family: sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 32px; }
main { max-width: 900px; margin: 0 auto; background: #111827; border: 1px solid #334155; border-radius: 16px; padding: 24px; }
.proof { padding: 14px; border-radius: 12px; background: #14532d; color: #dcfce7; }
.baseline { padding: 14px; border-radius: 12px; background: #1e3a8a; color: #dbeafe; }
code { background: rgba(255,255,255,0.08); padding: 2px 6px; border-radius: 6px; }
</style>
</head>
<body>
<main>
<h1>${scenario.title}</h1>
<p>${scenario.subtitle}</p>
${banner}
<p>System: <code>${scenario.system_id}</code> / Family: <code>${scenario.family}</code></p>
${admin}
${xssBlock}
${uploads}
${sink}
${deserialize}
</main>
</body>
</html>`;
}
function readBody(req) {
return new Promise((resolve) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
} catch (_error) {
resolve({});
}
});
});
}
async function handleAttack(payload) {
const family = payload.family || state.family;
state.case_id = payload.case_id || state.case_id;
state.payload = payload;
state.proof = true;
if (family === "proxy-boundary") {
state.detail = "trusted forwarded headers crossed the boundary";
} else if (family === "authz-bypass") {
state.detail = "server-side authorization recheck was bypassed";
} else if (family === "ssrf") {
await fetch(`http://127.0.0.1:${port}/sink?case_id=${encodeURIComponent(state.case_id)}`);
state.detail = "server-side callback reached the local sink";
} else if (family === "xss") {
state.detail = "stored payload rendered inside the browser proof page";
} else if (family === "file-upload") {
state.uploads.push({ filename: payload.filename || `${state.case_id}.txt`, content: payload.content || "" });
state.detail = "upload marker accepted and listed";
} else if (family === "deserialization") {
state.detail = "unsafe object graph decoded without gadget execution";
}
note("attack", state.detail);
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1:${port}`);
if (req.method === "GET" && url.pathname === "/healthz") {
sendJson(res, 200, { ok: true, system_id: scenario.system_id, family: scenario.family });
return;
}
if (req.method === "GET" && url.pathname === "/") {
const body = renderHtml();
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": Buffer.byteLength(body) });
res.end(body);
return;
}
if (req.method === "GET" && url.pathname === "/admin") {
if (state.proof && ["proxy-boundary", "authz-bypass"].includes(state.family)) {
sendJson(res, 200, { ok: true, detail: state.detail, case_id: state.case_id });
} else {
sendJson(res, 403, { ok: false, detail: "admin boundary still enforced" });
}
return;
}
if (req.method === "GET" && url.pathname === "/sink") {
state.sink_hits += 1;
note("sink-hit", url.searchParams.toString() || "local callback");
sendJson(res, 200, { ok: true, sink_hits: state.sink_hits });
return;
}
if (req.method === "GET" && url.pathname === "/proof") {
sendJson(res, 200, {
success: Boolean(state.proof),
detail: state.detail,
case_id: state.case_id,
sink_hits: state.sink_hits,
uploads: state.uploads,
events: state.events
});
return;
}
if (req.method === "POST" && url.pathname === "/seed") {
const payload = await readBody(req);
state.seeded = true;
state.proof = false;
state.case_id = String(payload.case_id || "");
state.detail = "fixture seeded";
state.uploads = [];
state.sink_hits = 0;
state.payload = null;
note("seed", state.case_id || "anonymous");
sendJson(res, 200, { ok: true, detail: "fixture seeded", case_id: state.case_id });
return;
}
if (req.method === "POST" && url.pathname === "/attack") {
const payload = await readBody(req);
await handleAttack(payload);
sendJson(res, 200, { ok: true, detail: state.detail, case_id: state.case_id });
return;
}
sendJson(res, 404, { ok: false, detail: "not found" });
});
server.listen(port, "0.0.0.0");