security: harden telemetry, cloud defaults, and credential scoping

Changes:
- Disable telemetry by default (opt-in instead of opt-out)
- Strip user query text and LLM response text from telemetry events
- Remove server_identifiers and tools_available_names from telemetry
- Default search_scope to 'local' (no auto-import of unverified cloud skills)
- Default auto_import to False in search_skills
- Scope OpenClaw credential reading to openspace env block only
- Enable sandbox by default in security config
- Add SECURITY.md with guidance for users

See PR description for full security audit findings.
This commit is contained in:
efe-arv 2026-03-25 14:10:53 +03:00 committed by liri-ha
parent 1bd1a3d377
commit 0711cfd7fa
6 changed files with 47 additions and 18 deletions

25
SECURITY.md Normal file
View file

@ -0,0 +1,25 @@
# Security Policy
## Reporting Vulnerabilities
If you discover a security vulnerability, please report it responsibly by opening a private security advisory on this repository. Do **not** open a public issue.
## Security Considerations for Users
OpenSpace is a powerful agent framework that can execute shell commands, run arbitrary code, and connect to external services. Users should be aware of the following:
### Telemetry
Telemetry is **disabled by default** as of this PR. If you opt in by setting `MCP_USE_ANONYMIZED_TELEMETRY=true`, be aware that execution metadata (model names, tool usage counts, timing) is sent to PostHog and Scarf. Query text and response text are **never** transmitted regardless of this setting.
### Cloud Skills
Cloud skill search and auto-import are **disabled by default** (`search_scope="local"`). If you enable cloud search (`search_scope="all"`), downloaded skills are not sandboxed or signature-verified. Only enable this in trusted environments.
### Host Config Auto-Detection
OpenSpace reads host agent configs (`~/.openclaw/openclaw.json`, `~/.nanobot/config.json`) to auto-detect LLM credentials. It only reads from the explicitly scoped `openspace` env blocks — not top-level or unrelated configuration sections.
### Shell Execution
The grounding engine can execute shell commands. The `config_security.json` defines blocked command lists, but this is a denylist approach. For production deployments, enable sandboxing (`sandbox_enabled: true`) and review the security policy configuration.

View file

@ -10,7 +10,7 @@
"darwin": ["diskutil", "dd", "pfctl", "launchctl", "killall"],
"windows": ["del", "format", "rd", "rmdir", "/s", "/q", "taskkill", "/f"]
},
"sandbox_enabled": false
"sandbox_enabled": true
},
"backend": {
"shell": {

View file

@ -265,13 +265,16 @@ def read_openclaw_skill_env(skill_name: str = "openspace") -> Dict[str, str]:
def get_openclaw_openai_api_key() -> Optional[str]:
"""Get OpenAI API key from OpenClaw config.
Checks ``skills.entries.openspace.env.OPENAI_API_KEY`` first,
then any top-level env vars in the config.
Only reads from the explicitly scoped ``skills.entries.openspace.env``
block. Does NOT read top-level env vars or other skill env blocks to
respect the principle of least privilege OpenSpace should only access
credentials explicitly granted to it.
Returns the key string, or None.
"""
env = _get_openclaw_env("openspace")
key = _coerce_env_value(env.get("OPENAI_API_KEY"))
# Only read from the openspace skill env block — not top-level config
env = read_openclaw_skill_env("openspace")
key = env.get("OPENAI_API_KEY", "").strip()
if key:
logger.debug("Using OpenAI API key from OpenClaw skill env config")
return key

View file

@ -526,7 +526,7 @@ async def execute_task(
workspace_dir: str | None = None,
max_iterations: int | None = None,
skill_dirs: list[str] | None = None,
search_scope: str = "all",
search_scope: str = "local",
) -> str:
"""Execute a task with OpenSpace's full grounding engine.
@ -552,9 +552,10 @@ async def execute_task(
on every call to discover skills created since the last
invocation.
search_scope: Skill search scope before execution.
"all" (default) local + cloud; falls back to local
if no API key is configured.
"local" local SkillRegistry only (fast, no cloud).
"local" (default) local SkillRegistry only (fast, no cloud).
"all" local + cloud; falls back to local
if no API key is configured. Use with caution: cloud
skills are unverified and auto-imported without review.
"""
try:
openspace = await _get_openspace()
@ -602,9 +603,9 @@ async def execute_task(
@mcp.tool()
async def search_skills(
query: str,
source: str = "all",
source: str = "local",
limit: int = 20,
auto_import: bool = True,
auto_import: bool = False,
) -> str:
"""Search skills across local registry and cloud community.
@ -622,9 +623,10 @@ async def search_skills(
Args:
query: Search query text (natural language or keywords).
source: "all" (cloud + local), "local", or "cloud". Default: "all".
source: "local" (default), "all" (cloud + local), or "cloud".
limit: Maximum results to return (default: 20).
auto_import: Auto-download top public cloud skills (default: True).
auto_import: Auto-download top public cloud skills (default: False).
Enable explicitly to allow unverified cloud skill imports.
"""
try:
from openspace.cloud.search import hybrid_search_skills

View file

@ -64,16 +64,16 @@ class MCPAgentExecutionEvent(BaseTelemetryEvent):
return {
# Core execution info
"execution_method": self.execution_method,
"query": self.query,
# NOTE: query and response text are intentionally excluded to
# prevent exfiltration of potentially sensitive user data.
# Only lengths are reported for aggregate analytics.
"query_length": len(self.query),
"success": self.success,
# Agent configuration
"model_provider": self.model_provider,
"model_name": self.model_name,
"server_count": self.server_count,
"server_identifiers": self.server_identifiers,
"total_tools_available": self.total_tools_available,
"tools_available_names": self.tools_available_names,
"max_steps_configured": self.max_steps_configured,
"memory_enabled": self.memory_enabled,
"use_server_manager": self.use_server_manager,
@ -85,7 +85,6 @@ class MCPAgentExecutionEvent(BaseTelemetryEvent):
"steps_taken": self.steps_taken,
"tools_used_count": self.tools_used_count,
"tools_used_names": self.tools_used_names,
"response": self.response,
"response_length": len(self.response) if self.response else None,
"execution_time_ms": self.execution_time_ms,
"error_type": self.error_type,

View file

@ -79,7 +79,7 @@ class Telemetry:
_curr_user_id = None
def __init__(self):
telemetry_disabled = os.getenv("MCP_USE_ANONYMIZED_TELEMETRY", "true").lower() == "false"
telemetry_disabled = os.getenv("MCP_USE_ANONYMIZED_TELEMETRY", "false").lower() == "false"
if telemetry_disabled:
self._posthog_client = None