Files
Claude-Code-Monitor/server/__tests__/security.test.js
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

163 lines
5.8 KiB
JavaScript

/**
* @file security.test.js
* @description Tests the network-exposure hardening (GHSA-gr74-4xfh-6jw9):
* loopback-by-default bind, Host-header allowlist (anti DNS-rebinding),
* loopback-only CORS, and the optional bearer-token gate on /api/* + WebSocket.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, afterEach } = require("node:test");
const assert = require("node:assert/strict");
const sec = require("../lib/security");
const ENV_KEYS = ["DASHBOARD_HOST", "DASHBOARD_ALLOWED_HOSTS", "DASHBOARD_TOKEN"];
afterEach(() => {
for (const k of ENV_KEYS) delete process.env[k];
});
function mockRes() {
return {
statusCode: 0,
body: null,
status(c) {
this.statusCode = c;
return this;
},
json(b) {
this.body = b;
return this;
},
};
}
describe("resolveHost", () => {
it("defaults to loopback (127.0.0.1)", () => {
assert.equal(sec.resolveHost(), "127.0.0.1");
});
it("honors an explicit DASHBOARD_HOST opt-in", () => {
process.env.DASHBOARD_HOST = "0.0.0.0";
assert.equal(sec.resolveHost(), "0.0.0.0");
assert.equal(sec.isLoopbackHostname("0.0.0.0"), true); // treated as loopback-equiv for Host checks
});
});
describe("Host allowlist (DNS-rebinding defense)", () => {
it("allows loopback Host headers", () => {
assert.equal(sec.isHostAllowed("localhost:4820"), true);
assert.equal(sec.isHostAllowed("127.0.0.1:4820"), true);
assert.equal(sec.isHostAllowed("[::1]:4820"), true);
assert.equal(sec.isHostAllowed(""), true); // missing Host (HTTP/1.0 / local tooling)
});
it("rejects a rebound attacker Host", () => {
assert.equal(sec.isHostAllowed("evil.example"), false);
assert.equal(sec.isHostAllowed("attacker.example:4820"), false);
});
it("permits operator-allowlisted hostnames", () => {
process.env.DASHBOARD_ALLOWED_HOSTS = "dash.internal, 192.168.1.50";
assert.equal(sec.isHostAllowed("dash.internal:4820"), true);
assert.equal(sec.isHostAllowed("192.168.1.50:4820"), true);
assert.equal(sec.isHostAllowed("evil.example"), false);
});
it("hostGuard middleware 403s a disallowed Host", () => {
const res = mockRes();
let nexted = false;
sec.hostGuard({ headers: { host: "evil.example" } }, res, () => (nexted = true));
assert.equal(nexted, false);
assert.equal(res.statusCode, 403);
assert.equal(res.body.error.code, "EBADHOST");
});
it("hostGuard middleware allows loopback", () => {
let nexted = false;
sec.hostGuard({ headers: { host: "localhost:4820" } }, mockRes(), () => (nexted = true));
assert.equal(nexted, true);
});
});
describe("CORS", () => {
const allowed = (origin) =>
new Promise((resolve) => sec.corsOptions().origin(origin, (_e, ok) => resolve(ok)));
it("allows same-origin / no-Origin (curl, the server's own client)", async () => {
assert.equal(await allowed(undefined), true);
});
it("allows loopback origins", async () => {
assert.equal(await allowed("http://localhost:5173"), true);
assert.equal(await allowed("http://127.0.0.1:4820"), true);
});
it("refuses cross-origin pages", async () => {
assert.equal(await allowed("https://evil.example"), false);
});
});
describe("token gate (optional, opt-in)", () => {
it("is a no-op when DASHBOARD_TOKEN is unset (default)", () => {
let nexted = false;
sec.tokenGuard({ path: "/stats", headers: {}, query: {} }, mockRes(), () => (nexted = true));
assert.equal(nexted, true);
});
it("rejects a missing/invalid token when configured", () => {
process.env.DASHBOARD_TOKEN = "s3cret";
const res = mockRes();
let nexted = false;
sec.tokenGuard({ path: "/stats", headers: {}, query: {} }, res, () => (nexted = true));
assert.equal(nexted, false);
assert.equal(res.statusCode, 401);
assert.equal(res.body.error.code, "EUNAUTHORIZED");
const res2 = mockRes();
sec.tokenGuard(
{ path: "/stats", headers: { "x-dashboard-token": "wrong" }, query: {} },
res2,
() => {}
);
assert.equal(res2.statusCode, 401);
});
it("accepts a correct token via header, bearer, or query", () => {
process.env.DASHBOARD_TOKEN = "s3cret";
const ok = (req) => {
let nexted = false;
sec.tokenGuard(req, mockRes(), () => (nexted = true));
return nexted;
};
assert.equal(
ok({ path: "/stats", headers: { "x-dashboard-token": "s3cret" }, query: {} }),
true
);
assert.equal(
ok({ path: "/stats", headers: { authorization: "Bearer s3cret" }, query: {} }),
true
);
assert.equal(ok({ path: "/stats", headers: {}, query: { token: "s3cret" } }), true);
});
it("exempts health, docs, and local hook ingestion even when a token is set", () => {
process.env.DASHBOARD_TOKEN = "s3cret";
const ok = (path) => {
let nexted = false;
sec.tokenGuard({ path, headers: {}, query: {} }, mockRes(), () => (nexted = true));
return nexted;
};
assert.equal(ok("/health"), true);
assert.equal(ok("/openapi.json"), true);
assert.equal(ok("/hooks/event"), true);
assert.equal(ok("/sessions/abc"), false); // still gated
});
});
describe("WebSocket auth", () => {
it("allows any upgrade when no token is configured", () => {
assert.equal(sec.isWebSocketAuthorized({ url: "/ws", headers: {} }), true);
});
it("requires a matching ?token= when configured", () => {
process.env.DASHBOARD_TOKEN = "s3cret";
assert.equal(sec.isWebSocketAuthorized({ url: "/ws?token=s3cret", headers: {} }), true);
assert.equal(sec.isWebSocketAuthorized({ url: "/ws?token=nope", headers: {} }), false);
assert.equal(sec.isWebSocketAuthorized({ url: "/ws", headers: {} }), false);
assert.equal(
sec.isWebSocketAuthorized({ url: "/ws", headers: { "x-dashboard-token": "s3cret" } }),
true
);
});
});