mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
fix(security-doctor): honor OPENCLAW_CALLBACK_ALLOW_HOSTS with legacy fallback and add SSRF env-key regression tests
This commit is contained in:
@@ -14,6 +14,16 @@ Use this guide together with:
|
||||
- `docs/deploy/reverse-proxy.md`
|
||||
- `docs/security_checklist.md`
|
||||
|
||||
## 0. Pre-deployment Disclaimer (Public Exposure)
|
||||
|
||||
Before using OpenClaw in any internet-facing setup, you must explicitly accept:
|
||||
|
||||
1. This project is local-first by design; exposing it to public networks increases attack surface.
|
||||
2. This guide reduces risk but does not guarantee security, compliance, or incident-free operation.
|
||||
3. The operator/deployer is responsible for network isolation, auth boundaries, key management, monitoring, and incident response.
|
||||
4. If you cannot satisfy the `public` profile baseline and checklist, do not deploy publicly. Use `local` or private/VPN-only access instead.
|
||||
5. High-risk capabilities (external tools, registry sync, transforms, remote admin) must remain disabled unless there is a reviewed and time-bounded operational requirement.
|
||||
|
||||
## 1. Profile Matrix
|
||||
|
||||
| Profile | Intended Use | Minimum Security Baseline |
|
||||
|
||||
@@ -397,13 +397,18 @@ def check_token_boundaries(report: SecurityReport) -> None:
|
||||
|
||||
def check_ssrf_posture(report: SecurityReport) -> None:
|
||||
"""Check callback/base_url configurations for SSRF risk indicators."""
|
||||
# Check OPENCLAW_CALLBACK_ALLOWLIST for wildcard abuse
|
||||
callback_allowlist = os.environ.get(
|
||||
"OPENCLAW_CALLBACK_ALLOWLIST", ""
|
||||
) or os.environ.get("MOLTBOT_CALLBACK_ALLOWLIST", "")
|
||||
# CRITICAL: keep canonical *_ALLOW_HOSTS keys first. Runtime callback policy
|
||||
# and deployment-profile checks use these names; drifting to legacy-only
|
||||
# aliases makes Security Doctor miss live SSRF posture violations.
|
||||
callback_allowlist = (
|
||||
os.environ.get("OPENCLAW_CALLBACK_ALLOW_HOSTS", "").strip()
|
||||
or os.environ.get("MOLTBOT_CALLBACK_ALLOW_HOSTS", "").strip()
|
||||
or os.environ.get("OPENCLAW_CALLBACK_ALLOWLIST", "").strip()
|
||||
or os.environ.get("MOLTBOT_CALLBACK_ALLOWLIST", "").strip()
|
||||
)
|
||||
if callback_allowlist:
|
||||
hosts = [h.strip() for h in callback_allowlist.split(",") if h.strip()]
|
||||
if "*" in hosts or "*.com" in hosts or "*.net" in hosts:
|
||||
if any("*" in host for host in hosts):
|
||||
report.add(
|
||||
SecurityCheckResult(
|
||||
name="callback_wildcard",
|
||||
|
||||
@@ -161,6 +161,68 @@ class TestSecurityDoctor(unittest.TestCase):
|
||||
else:
|
||||
os.environ[key] = val
|
||||
|
||||
def test_ssrf_posture_prefers_callback_allow_hosts(self):
|
||||
"""S30: SSRF posture must read canonical callback allow-host env keys."""
|
||||
from services.security_doctor import SecurityReport, check_ssrf_posture
|
||||
|
||||
keys = (
|
||||
"OPENCLAW_CALLBACK_ALLOW_HOSTS",
|
||||
"MOLTBOT_CALLBACK_ALLOW_HOSTS",
|
||||
"OPENCLAW_CALLBACK_ALLOWLIST",
|
||||
"MOLTBOT_CALLBACK_ALLOWLIST",
|
||||
)
|
||||
old_env = {k: os.environ.get(k) for k in keys}
|
||||
|
||||
try:
|
||||
for k in keys:
|
||||
os.environ.pop(k, None)
|
||||
os.environ["OPENCLAW_CALLBACK_ALLOW_HOSTS"] = "example.com,api.example.com"
|
||||
|
||||
report = SecurityReport()
|
||||
check_ssrf_posture(report)
|
||||
allowlist = next(
|
||||
(c for c in report.checks if c.name == "callback_allowlist"), None
|
||||
)
|
||||
self.assertIsNotNone(allowlist)
|
||||
self.assertEqual(allowlist.severity, "pass")
|
||||
finally:
|
||||
for key, val in old_env.items():
|
||||
if val is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = val
|
||||
|
||||
def test_ssrf_posture_legacy_allowlist_alias_still_supported(self):
|
||||
"""S30: keep backward compatibility for legacy callback allowlist keys."""
|
||||
from services.security_doctor import SecurityReport, check_ssrf_posture
|
||||
|
||||
keys = (
|
||||
"OPENCLAW_CALLBACK_ALLOW_HOSTS",
|
||||
"MOLTBOT_CALLBACK_ALLOW_HOSTS",
|
||||
"OPENCLAW_CALLBACK_ALLOWLIST",
|
||||
"MOLTBOT_CALLBACK_ALLOWLIST",
|
||||
)
|
||||
old_env = {k: os.environ.get(k) for k in keys}
|
||||
|
||||
try:
|
||||
for k in keys:
|
||||
os.environ.pop(k, None)
|
||||
os.environ["OPENCLAW_CALLBACK_ALLOWLIST"] = "legacy.example.com"
|
||||
|
||||
report = SecurityReport()
|
||||
check_ssrf_posture(report)
|
||||
allowlist = next(
|
||||
(c for c in report.checks if c.name == "callback_allowlist"), None
|
||||
)
|
||||
self.assertIsNotNone(allowlist)
|
||||
self.assertEqual(allowlist.severity, "pass")
|
||||
finally:
|
||||
for key, val in old_env.items():
|
||||
if val is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = val
|
||||
|
||||
def test_redaction_drift_check(self):
|
||||
"""S30: verify redaction coverage passes."""
|
||||
from services.security_doctor import SecurityReport, check_redaction_drift
|
||||
|
||||
Reference in New Issue
Block a user