"""
Sandbox Defaults Security Tests
Tests for secure-by-default sandbox configuration, environment filtering,
and fetch API network guard.
Test Coverage:
- Default sandbox enabled
- Default network blocked
- Default CPU, memory, timeout limits
- Environment variable filtering (allowlist)
- Fetch API guard in network guard JS
"""
import os
from unittest.mock import patch
import pytest
from canvas_mcp.core.config import Config
from canvas_mcp.tools.code_execution import (
_SAFE_ENV_KEYS,
_build_safe_env,
_write_network_guard,
)
class TestSandboxDefaults:
"""Test that sandbox defaults are secure."""
def test_default_sandbox_enabled(self):
"""ENABLE_TS_SANDBOX defaults to True."""
with patch.dict(os.environ, {}, clear=True):
# Provide required env vars
env = {"CANVAS_API_TOKEN": "test", "CANVAS_API_URL": "https://x.com/api/v1"}
with patch.dict(os.environ, env):
config = Config()
assert config.enable_ts_sandbox is True
def test_default_network_blocked(self):
"""TS_SANDBOX_BLOCK_OUTBOUND_NETWORK defaults to True."""
with patch.dict(os.environ, {
"CANVAS_API_TOKEN": "test",
"CANVAS_API_URL": "https://x.com/api/v1",
}):
config = Config()
assert config.ts_sandbox_block_outbound_network is True
def test_default_cpu_limit(self):
"""TS_SANDBOX_CPU_LIMIT defaults to 30."""
with patch.dict(os.environ, {
"CANVAS_API_TOKEN": "test",
"CANVAS_API_URL": "https://x.com/api/v1",
}):
config = Config()
assert config.ts_sandbox_cpu_limit == 30
def test_default_memory_limit(self):
"""TS_SANDBOX_MEMORY_LIMIT_MB defaults to 512."""
with patch.dict(os.environ, {
"CANVAS_API_TOKEN": "test",
"CANVAS_API_URL": "https://x.com/api/v1",
}):
config = Config()
assert config.ts_sandbox_memory_limit_mb == 512
def test_default_timeout(self):
"""TS_SANDBOX_TIMEOUT_SEC defaults to 120."""
with patch.dict(os.environ, {
"CANVAS_API_TOKEN": "test",
"CANVAS_API_URL": "https://x.com/api/v1",
}):
config = Config()
assert config.ts_sandbox_timeout_sec == 120
class TestEnvironmentFiltering:
"""Test subprocess environment variable filtering."""
def test_env_filtering_excludes_secrets(self):
"""Arbitrary env vars (like DB passwords, AWS keys) are excluded."""
with patch.dict(os.environ, {
"SECRET_KEY": "supersecret",
"AWS_SECRET_ACCESS_KEY": "awskey",
"DATABASE_URL": "postgres://...",
"PATH": "/usr/bin",
"CANVAS_API_TOKEN": "tok",
"CANVAS_API_URL": "https://x.com/api/v1",
}):
config = Config()
env = _build_safe_env(config)
assert "SECRET_KEY" not in env
assert "AWS_SECRET_ACCESS_KEY" not in env
assert "DATABASE_URL" not in env
# Canvas credentials should be present (explicitly added)
assert env["CANVAS_API_TOKEN"] == "tok"
def test_env_filtering_includes_path(self):
"""PATH is preserved in the subprocess environment."""
with patch.dict(os.environ, {
"PATH": "/usr/local/bin:/usr/bin",
"CANVAS_API_TOKEN": "tok",
"CANVAS_API_URL": "https://x.com/api/v1",
}):
config = Config()
env = _build_safe_env(config)
assert "PATH" in env
assert env["PATH"] == "/usr/local/bin:/usr/bin"
def test_safe_env_keys_are_reasonable(self):
"""Verify the allowlist contains expected system keys."""
assert "PATH" in _SAFE_ENV_KEYS
assert "HOME" in _SAFE_ENV_KEYS
assert "NODE_PATH" in _SAFE_ENV_KEYS
assert "TMPDIR" in _SAFE_ENV_KEYS
# Should NOT contain sensitive patterns
for key in _SAFE_ENV_KEYS:
assert "SECRET" not in key.upper()
assert "TOKEN" not in key.upper()
assert "PASSWORD" not in key.upper()
class TestFetchGuard:
"""Test that the network guard JS includes globalThis.fetch interception."""
def test_fetch_guard_generated(self, tmp_path):
"""Guard JS should include globalThis.fetch override."""
guard_path = _write_network_guard(["canvas.example.com"], tmp_path)
try:
content = guard_path.read_text()
assert "globalThis.fetch" in content
assert "originalFetch" in content
assert "enforce" in content
finally:
guard_path.unlink()
if __name__ == "__main__":
pytest.main([__file__, "-v"])