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:
37
cli/src/commands/allowed-hostname.ts
Normal file
37
cli/src/commands/allowed-hostname.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { normalizeHostnameInput } from "../config/hostnames.js";
|
||||
import { readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
||||
|
||||
export async function addAllowedHostname(host: string, opts: { config?: string }): Promise<void> {
|
||||
const configPath = resolveConfigPath(opts.config);
|
||||
const config = readConfig(opts.config);
|
||||
|
||||
if (!config) {
|
||||
p.log.error(`No config found at ${configPath}. Run ${pc.cyan("paperclip onboard")} first.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeHostnameInput(host);
|
||||
const current = new Set((config.server.allowedHostnames ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean));
|
||||
const existed = current.has(normalized);
|
||||
current.add(normalized);
|
||||
|
||||
config.server.allowedHostnames = Array.from(current).sort();
|
||||
config.$meta.updatedAt = new Date().toISOString();
|
||||
config.$meta.source = "configure";
|
||||
writeConfig(config, opts.config);
|
||||
|
||||
if (existed) {
|
||||
p.log.info(`Hostname ${pc.cyan(normalized)} is already allowed.`);
|
||||
} else {
|
||||
p.log.success(`Added allowed hostname: ${pc.cyan(normalized)}`);
|
||||
}
|
||||
|
||||
if (!(config.server.deploymentMode === "authenticated" && config.server.exposure === "private")) {
|
||||
p.log.message(
|
||||
pc.dim("Note: allowed hostnames are enforced only in authenticated/private mode."),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ function defaultConfig(): PaperclipConfig {
|
||||
exposure: "private",
|
||||
host: "127.0.0.1",
|
||||
port: 3100,
|
||||
allowedHostnames: [],
|
||||
serveUi: true,
|
||||
},
|
||||
auth: {
|
||||
@@ -131,7 +132,10 @@ export async function configure(opts: {
|
||||
break;
|
||||
case "server":
|
||||
{
|
||||
const { server, auth } = await promptServer();
|
||||
const { server, auth } = await promptServer({
|
||||
currentServer: config.server,
|
||||
currentAuth: config.auth,
|
||||
});
|
||||
config.server = server;
|
||||
config.auth = auth;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ export async function onboard(opts: { config?: string }): Promise<void> {
|
||||
llm ? `LLM: ${llm.provider}` : "LLM: not configured",
|
||||
`Logging: ${logging.mode} → ${logging.logDir}`,
|
||||
`Server: ${server.deploymentMode}/${server.exposure} @ ${server.host}:${server.port}`,
|
||||
`Allowed hosts: ${server.allowedHostnames.length > 0 ? server.allowedHostnames.join(", ") : "(loopback only)"}`,
|
||||
`Auth URL mode: ${auth.baseUrlMode}${auth.publicBaseUrl ? ` (${auth.publicBaseUrl})` : ""}`,
|
||||
`Storage: ${storage.provider}`,
|
||||
`Secrets: ${secrets.provider} (strict mode ${secrets.strictMode ? "on" : "off"})`,
|
||||
|
||||
Reference in New Issue
Block a user