feat(security): warn when gateway.tools.allow re-enables dangerous HTTP tools

This commit is contained in:
Peter Steinberger
2026-02-14 12:44:43 +01:00
parent fba19fe942
commit 539689a2f2
2 changed files with 74 additions and 0 deletions

View File

@@ -153,6 +153,53 @@ describe("security audit", () => {
).toBe(true);
});
it("warns when gateway.tools.allow re-enables dangerous HTTP /tools/invoke tools (loopback)", async () => {
const cfg: OpenClawConfig = {
gateway: {
bind: "loopback",
auth: { token: "secret" },
tools: { allow: ["sessions_spawn"] },
},
};
const res = await runSecurityAudit({
config: cfg,
env: {},
includeFilesystem: false,
includeChannelSecurity: false,
});
expect(
res.findings.some(
(f) => f.checkId === "gateway.tools_invoke_http.dangerous_allow" && f.severity === "warn",
),
).toBe(true);
});
it("flags dangerous gateway.tools.allow over HTTP as critical when gateway binds beyond loopback", async () => {
const cfg: OpenClawConfig = {
gateway: {
bind: "lan",
auth: { token: "secret" },
tools: { allow: ["sessions_spawn", "gateway"] },
},
};
const res = await runSecurityAudit({
config: cfg,
env: {},
includeFilesystem: false,
includeChannelSecurity: false,
});
expect(
res.findings.some(
(f) =>
f.checkId === "gateway.tools_invoke_http.dangerous_allow" && f.severity === "critical",
),
).toBe(true);
});
it("does not warn for auth rate limiting when configured", async () => {
const cfg: OpenClawConfig = {
gateway: {

View File

@@ -258,6 +258,33 @@ function collectGatewayConfigFindings(
(auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
// HTTP /tools/invoke is intended for narrow automation, not session orchestration/admin operations.
// If operators opt-in to re-enabling these tools over HTTP, warn loudly so the choice is explicit.
const gatewayToolsAllowRaw = Array.isArray(cfg.gateway?.tools?.allow)
? cfg.gateway?.tools?.allow
: [];
const gatewayToolsAllow = new Set(
gatewayToolsAllowRaw
.map((v) => (typeof v === "string" ? v.trim().toLowerCase() : ""))
.filter(Boolean),
);
const defaultHttpDeniedTools = ["sessions_spawn", "sessions_send", "gateway", "whatsapp_login"];
const reenabledOverHttp = defaultHttpDeniedTools.filter((name) => gatewayToolsAllow.has(name));
if (reenabledOverHttp.length > 0) {
const extraRisk = bind !== "loopback" || tailscaleMode === "funnel";
findings.push({
checkId: "gateway.tools_invoke_http.dangerous_allow",
severity: extraRisk ? "critical" : "warn",
title: "Gateway HTTP /tools/invoke re-enables dangerous tools",
detail:
`gateway.tools.allow includes ${reenabledOverHttp.join(", ")} which removes them from the default HTTP deny list. ` +
"This can allow remote session spawning / control-plane actions via HTTP and increases RCE blast radius if the gateway is reachable.",
remediation:
"Remove these entries from gateway.tools.allow (recommended). " +
"If you keep them enabled, keep gateway.bind loopback-only (or tailnet-only), restrict network exposure, and treat the gateway token/password as full-admin.",
});
}
if (bind !== "loopback" && !hasSharedSecret && auth.mode !== "trusted-proxy") {
findings.push({
checkId: "gateway.bind_no_auth",