mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
feat(security): warn when gateway.tools.allow re-enables dangerous HTTP tools
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user