export function collectSyncedFolderFindings(params: {
stateDir: string;
configPath: string;
}): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; if (isProbablySyncedPath(params.stateDir) || isProbablySyncedPath(params.configPath)) {
findings.push({
checkId: "fs.synced_dir",
severity: "warn",
title: "State/config path looks like a synced folder",
detail: `stateDir=${params.stateDir}, configPath=${params.configPath}. Synced folders (iCloud/Dropbox/OneDrive/Google Drive) can leak tokens and transcripts onto other devices.`,
remediation: `Keep OPENCLAW_STATE_DIR on a local-only volume and re-run "${formatCliCommand("openclaw security audit --fix")}".`,
});
} return findings;
}
export function collectSecretsInConfigFindings(cfg: OpenClawConfig): SecurityAuditFinding[] { const findings: SecurityAuditFinding[] = []; const password = normalizeOptionalString(cfg.gateway?.auth?.password) ?? ""; if (password && !looksLikeEnvRef(password)) {
findings.push({
checkId: "config.secrets.gateway_password_in_config",
severity: "warn",
title: "Gateway password is stored in config",
detail: "gateway.auth.password is set in the config file; prefer environment variables for secrets when possible.",
remediation: "Prefer OPENCLAW_GATEWAY_PASSWORD (env) and remove gateway.auth.password from disk.",
});
}
const hooksToken = normalizeOptionalString(cfg.hooks?.token) ?? ""; if (cfg.hooks?.enabled === true && hooksToken && !looksLikeEnvRef(hooksToken)) {
findings.push({
checkId: "config.secrets.hooks_token_in_config",
severity: "info",
title: "Hooks token is stored in config",
detail: "hooks.token is set in the config file; keep config perms tight and treat it like an API secret.",
});
}
const rawPath = normalizeOptionalString(cfg.hooks?.path) ?? ""; if (rawPath === "/") {
findings.push({
checkId: "hooks.path_root",
severity: "critical",
title: "Hooks base path is '/'",
detail: "hooks.path='/' would shadow other HTTP endpoints and is unsafe.",
remediation: "Use a dedicated path like '/hooks'.",
});
}
if (!defaultSessionKey) {
findings.push({
checkId: "hooks.default_session_key_unset",
severity: "warn",
title: "hooks.defaultSessionKey is not configured",
detail: "Hook agent runs without explicit sessionKey use generated per-request keys. Set hooks.defaultSessionKey to keep hook ingress scoped to a known session.",
remediation: 'Set hooks.defaultSessionKey (for example, "hook:ingress").',
});
}
if (allowedAgentIds === undefined) {
findings.push({
checkId: "hooks.allowed_agent_ids_unrestricted",
severity: remoteExposure ? "critical" : "warn",
title: "Hook agent routing allows any configured agent",
detail: "hooks.allowedAgentIds is unset or includes '*', so authenticated hook callers may route to any configured agent id.",
remediation: 'Set hooks.allowedAgentIds to an explicit allowlist (for example, ["hooks", "main"]) or [] to deny explicit agent routing.',
});
}
if (allowRequestSessionKey) {
findings.push({
checkId: "hooks.request_session_key_enabled",
severity: remoteExposure ? "critical" : "warn",
title: "External hook payloads may override sessionKey",
detail: "hooks.allowRequestSessionKey=true allows `/hooks/agent` callers to choose the session key. Treat hook token holders as full-trust unless you also restrict prefixes.",
remediation: "Set hooks.allowRequestSessionKey=false (recommended) or constrain hooks.allowedSessionKeyPrefixes.",
});
}
if (allowRequestSessionKey && allowedPrefixes.length === 0) {
findings.push({
checkId: "hooks.request_session_key_prefixes_missing",
severity: remoteExposure ? "critical" : "warn",
title: "Request sessionKey override is enabled without prefix restrictions",
detail: "hooks.allowRequestSessionKey=true and hooks.allowedSessionKeyPrefixes is unset/empty, so request payloads can target arbitrary session key shapes.",
remediation: 'Set hooks.allowedSessionKeyPrefixes (for example, ["hook:"]) or disable request overrides.',
});
}
const remoteExposure = isGatewayRemotelyExposed(cfg);
findings.push({
checkId: "gateway.http.no_auth",
severity: remoteExposure ? "critical" : "warn",
title: "Gateway HTTP APIs are reachable without auth",
detail:
`gateway.auth.mode="none" leaves ${enabledEndpoints.join(", ")} callable without a shared secret. ` + "Treat this as trusted-local only and avoid exposing the gateway beyond loopback.",
remediation: "Set gateway.auth.mode to token/password (recommended). If you intentionally keep mode=none, keep gateway.bind=loopback and disable optional HTTP endpoints.",
});
for (const entry of agents) { if (!entry || typeof entry !== "object" || typeof entry.id !== "string") { continue;
} if (!hasConfiguredDockerConfig(entry.sandbox?.docker as Record<string, unknown> | undefined)) { continue;
} if (resolveSandboxConfigForAgent(cfg, entry.id).mode === "off") {
configuredPaths.push(`agents.list.${entry.id}.sandbox.docker`);
}
}
if (configuredPaths.length === 0) { return findings;
}
findings.push({
checkId: "sandbox.docker_config_mode_off",
severity: "warn",
title: "Sandbox docker settings configured while sandbox mode is off",
detail: "These docker settings will not take effect until sandbox mode is enabled:\n" +
configuredPaths.map((entry) => `- ${entry}`).join("\n"),
remediation: 'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) where needed, or remove unused docker settings.',
});
for (const { source, docker } of configs) { const binds = Array.isArray(docker.binds) ? docker.binds : []; for (const bind of binds) { if (typeof bind !== "string") { continue;
} const blocked = getBlockedBindReason(bind); if (!blocked) { continue;
} if (blocked.kind === "non_absolute") {
findings.push({
checkId: "sandbox.bind_mount_non_absolute",
severity: "warn",
title: "Sandbox bind mount uses a non-absolute source path",
detail:
`${source}.binds contains "${bind}" which uses source path "${blocked.sourcePath}". ` + "Non-absolute bind sources are hard to validate safely and may resolve unexpectedly.",
remediation: `Rewrite "${bind}" to use an absolute host path (for example: /home/user/project:/project:ro).`,
}); continue;
} if (blocked.kind !== "covers" && blocked.kind !== "targets") { continue;
} const verb = blocked.kind === "covers" ? "covers" : "targets";
findings.push({
checkId: "sandbox.dangerous_bind_mount",
severity: "critical",
title: "Dangerous bind mount in sandbox config",
detail:
`${source}.binds contains "${bind}" which ${verb} blocked path "${blocked.blockedPath}". ` + "This can expose host system directories or the Docker socket to sandbox containers.",
remediation: `Remove "${bind}" from ${source}.binds. Use project-specific paths instead.`,
});
}
const network = typeof docker.network === "string" ? docker.network : undefined; const normalizedNetwork = normalizeNetworkMode(network); if (isDangerousNetworkMode(network)) { const modeLabel = normalizedNetwork === "host" ? '"host"' : `"${network}"`; const detail =
normalizedNetwork === "host"
? `${source}.network is "host" which bypasses container network isolation entirely.`
: `${source}.network is ${modeLabel} which joins another container namespace and can bypass sandbox network isolation.`;
findings.push({
checkId: "sandbox.dangerous_network_mode",
severity: "critical",
title: "Dangerous network mode in sandbox config",
detail,
remediation:
`Set ${source}.network to "bridge", "none", or a custom bridge network name.` +
` Use ${source}.dangerouslyAllowContainerNamespaceJoin=true only as a break-glass override when you fully trust this runtime.`,
});
}
const seccompProfile = typeof docker.seccompProfile === "string" ? docker.seccompProfile : undefined; if (normalizeOptionalLowercaseString(seccompProfile) === "unconfined") {
findings.push({
checkId: "sandbox.dangerous_seccomp_profile",
severity: "critical",
title: "Seccomp unconfined in sandbox config",
detail: `${source}.seccompProfile is "unconfined" which disables syscall filtering.`,
remediation: `Remove ${source}.seccompProfile or use a custom seccomp profile file.`,
});
}
const apparmorProfile = typeof docker.apparmorProfile === "string" ? docker.apparmorProfile : undefined; if (normalizeOptionalLowercaseString(apparmorProfile) === "unconfined") {
findings.push({
checkId: "sandbox.dangerous_apparmor_profile",
severity: "critical",
title: "AppArmor unconfined in sandbox config",
detail: `${source}.apparmorProfile is "unconfined" which disables AppArmor enforcement.`,
remediation: `Remove ${source}.apparmorProfile or use a named AppArmor profile.`,
});
}
}
// CDP source range is now auto-derived at runtime from the Docker network gateway // for all bridge-like networks, so an unset cdpSourceRange is no longer a security gap.
findings.push({
checkId: "tools.profile_minimal_overridden",
severity: "warn",
title: "Global tools.profile=minimal is overridden by agent profiles",
detail: "Global minimal profile is set, but these agent profiles take precedence:\n" +
overrides.map((entry) => `- agents.list.${entry}`).join("\n"),
remediation: 'Set those agents to `tools.profile="minimal"` (or remove the agent override) if you want minimal tools enforced globally.',
});
for (const entry of models) { for (const pat of WEAK_TIER_MODEL_PATTERNS) { if (pat.re.test(entry.id)) {
addWeakMatch(entry.id, entry.source, pat.label); break;
}
} if (isGptModel(entry.id) && !isGpt5OrHigher(entry.id)) {
addWeakMatch(entry.id, entry.source, "Below GPT-5 family");
} if (isClaudeModel(entry.id) && !isClaude45OrHigher(entry.id)) {
addWeakMatch(entry.id, entry.source, "Below Claude 4.5");
}
}
const matches: Array<{ model: string; source: string; reason: string }> = []; for (const entry of models) { for (const pat of LEGACY_MODEL_PATTERNS) { if (pat.re.test(entry.id)) {
matches.push({ model: entry.id, source: entry.source, reason: pat.label }); break;
}
}
}
if (matches.length > 0) { const lines = matches
.slice(0, 12)
.map((m) => `- ${m.model} (${m.reason}) @ ${m.source}`)
.join("\n"); const more = matches.length > 12 ? `\n…${matches.length - 12} more` : "";
findings.push({
checkId: "models.legacy",
severity: "warn",
title: "Some configured models look legacy",
detail: "Older/legacy models can be less robust against prompt injection and tool misuse.\n" +
lines +
more,
remediation: "Prefer modern, instruction-hardened models for any bot that can run tools.",
});
}
if (weakMatches.size > 0) { const lines = Array.from(weakMatches.values())
.slice(0, 12)
.map((m) => `- ${m.model} (${m.reasons.join("; ")}) @ ${m.source}`)
.join("\n"); const more = weakMatches.size > 12 ? `\n…${weakMatches.size - 12} more` : "";
findings.push({
checkId: "models.weak_tier",
severity: "warn",
title: "Some configured models are below recommended tiers",
detail: "Smaller/older models are generally more susceptible to prompt injection and tool misuse.\n" +
lines +
more,
remediation: "Use the latest, top-tier model for any bot with tools or untrusted inboxes. Avoid Haiku tiers; prefer GPT-5+ and Claude 4.5+.",
});
}
const { riskyContexts, hasRuntimeRisk } = collectRiskyToolExposureContexts(cfg); const impactLine = hasRuntimeRisk
? "Runtime/process tools are exposed without full sandboxing in at least one context."
: "No unguarded runtime/process tools were detected by this heuristic."; const riskyContextsDetail =
riskyContexts.length > 0
? `Potential high-impact tool exposure contexts:\n${riskyContexts.map((line) => `- ${line}`).join("\n")}`
: "No unguarded runtime/filesystem contexts detected.";
findings.push({
checkId: "security.trust_model.multi_user_heuristic",
severity: "warn",
title: "Potential multi-user setup detected (personal-assistant model warning)",
detail: "Heuristic signals indicate this gateway may be reachable by multiple users:\n" +
signals.map((signal) => `- ${signal}`).join("\n") +
`\n${impactLine}\n${riskyContextsDetail}\n` + "OpenClaw's default security model is personal-assistant (one trusted operator boundary), not hostile multi-tenant isolation on one shared gateway.",
remediation: 'If users may be mutually untrusted, split trust boundaries (separate gateways + credentials, ideally separate OS users/hosts). If you intentionally run shared-user access, set agents.defaults.sandbox.mode="all", keep tools.fs.workspaceOnly=true, deny runtime/fs/web tools unless required, and keep personal/private identities + credentials off that runtime.',
});
return findings;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.32 Sekunden
(vorverarbeitet am 2026-06-07)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.