fix(security-doctor): honor OPENCLAW_CALLBACK_ALLOW_HOSTS with legacy fallback and add SSRF env-key regression tests

This commit is contained in:
rookiestar28
2026-02-18 05:17:36 +08:00
parent 9554d2a614
commit 4c91d90abc
3 changed files with 82 additions and 5 deletions
+10
View File
@@ -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 |
+10 -5
View File
@@ -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