feat: private hostname guard for authenticated/private mode

Reject requests from unrecognised Host headers when running
authenticated/private. Adds server middleware, CLI `allowed-hostname`
command, config-schema field, and prompt support for configuring
allowed hostnames during onboard/configure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-23 19:43:52 -06:00
parent 076092685e
commit 85c0b9a3dc
15 changed files with 385 additions and 8 deletions

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import express from "express";
import request from "supertest";
import { privateHostnameGuard } from "../middleware/private-hostname-guard.js";
function createApp(opts: { enabled: boolean; allowedHostnames?: string[]; bindHost?: string }) {
const app = express();
app.use(
privateHostnameGuard({
enabled: opts.enabled,
allowedHostnames: opts.allowedHostnames ?? [],
bindHost: opts.bindHost ?? "0.0.0.0",
}),
);
app.get("/api/health", (_req, res) => {
res.status(200).json({ status: "ok" });
});
app.get("/dashboard", (_req, res) => {
res.status(200).send("ok");
});
return app;
}
describe("privateHostnameGuard", () => {
it("allows requests when disabled", async () => {
const app = createApp({ enabled: false });
const res = await request(app).get("/api/health").set("Host", "dotta-macbook-pro:3100");
expect(res.status).toBe(200);
});
it("allows loopback hostnames", async () => {
const app = createApp({ enabled: true });
const res = await request(app).get("/api/health").set("Host", "localhost:3100");
expect(res.status).toBe(200);
});
it("allows explicitly configured hostnames", async () => {
const app = createApp({ enabled: true, allowedHostnames: ["dotta-macbook-pro"] });
const res = await request(app).get("/api/health").set("Host", "dotta-macbook-pro:3100");
expect(res.status).toBe(200);
});
it("blocks unknown hostnames with remediation command", async () => {
const app = createApp({ enabled: true, allowedHostnames: ["some-other-host"] });
const res = await request(app).get("/api/health").set("Host", "dotta-macbook-pro:3100");
expect(res.status).toBe(403);
expect(res.body?.error).toContain("please run pnpm paperclip allowed-hostname dotta-macbook-pro");
});
it("blocks unknown hostnames on page routes with plain-text remediation command", async () => {
const app = createApp({ enabled: true, allowedHostnames: ["some-other-host"] });
const res = await request(app).get("/dashboard").set("Host", "dotta-macbook-pro:3100");
expect(res.status).toBe(403);
expect(res.text).toContain("please run pnpm paperclip allowed-hostname dotta-macbook-pro");
});
});