489 lines
10 KiB
Markdown
489 lines
10 KiB
Markdown
|
|
# SECURITY FIXES CHECKLIST
|
||
|
|
|
||
|
|
## 20+ Specific Security Fixes Required
|
||
|
|
|
||
|
|
This document provides a detailed checklist of all security fixes identified in the comprehensive audit.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## CRITICAL FIXES (Must implement immediately)
|
||
|
|
|
||
|
|
### Fix 1: Remove shell=True from subprocess calls
|
||
|
|
**File:** `tools/terminal_tool.py`
|
||
|
|
**Line:** ~457
|
||
|
|
**CVSS:** 9.8
|
||
|
|
|
||
|
|
```python
|
||
|
|
# BEFORE
|
||
|
|
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ...)
|
||
|
|
|
||
|
|
# AFTER
|
||
|
|
# Validate command first
|
||
|
|
if not is_safe_command(exec_command):
|
||
|
|
raise SecurityError("Dangerous command detected")
|
||
|
|
subprocess.Popen(cmd_list, shell=False, ...) # Pass as list
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 2: Implement path sandbox validation
|
||
|
|
**File:** `tools/file_operations.py`
|
||
|
|
**Lines:** 409-420
|
||
|
|
**CVSS:** 9.1
|
||
|
|
|
||
|
|
```python
|
||
|
|
# BEFORE
|
||
|
|
def _expand_path(self, path: str) -> str:
|
||
|
|
if path.startswith('~'):
|
||
|
|
return os.path.expanduser(path)
|
||
|
|
return path
|
||
|
|
|
||
|
|
# AFTER
|
||
|
|
def _expand_path(self, path: str) -> Path:
|
||
|
|
safe_root = Path(self.cwd).resolve()
|
||
|
|
expanded = Path(path).expanduser().resolve()
|
||
|
|
if not str(expanded).startswith(str(safe_root)):
|
||
|
|
raise PermissionError(f"Path {path} outside allowed directory")
|
||
|
|
return expanded
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 3: Environment variable sanitization
|
||
|
|
**File:** `tools/code_execution_tool.py`
|
||
|
|
**Lines:** 434-461
|
||
|
|
**CVSS:** 9.3
|
||
|
|
|
||
|
|
```python
|
||
|
|
# BEFORE
|
||
|
|
_SAFE_ENV_PREFIXES = ("PATH", "HOME", "USER", ...)
|
||
|
|
_SECRET_SUBSTRINGS = ("TOKEN", "SECRET", ...)
|
||
|
|
|
||
|
|
# AFTER
|
||
|
|
_ALLOWED_ENV_VARS = frozenset([
|
||
|
|
"PATH", "HOME", "USER", "LANG", "LC_ALL",
|
||
|
|
"TERM", "SHELL", "PWD", "PYTHONPATH"
|
||
|
|
])
|
||
|
|
child_env = {k: v for k, v in os.environ.items()
|
||
|
|
if k in _ALLOWED_ENV_VARS}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 4: Secure sudo password handling
|
||
|
|
**File:** `tools/terminal_tool.py`
|
||
|
|
**Line:** 275
|
||
|
|
**CVSS:** 9.0
|
||
|
|
|
||
|
|
```python
|
||
|
|
# BEFORE
|
||
|
|
exec_command = f"printf '%s\\n' {shlex.quote(sudo_stdin.rstrip())} | {exec_command}"
|
||
|
|
|
||
|
|
# AFTER
|
||
|
|
# Use file descriptor passing instead of command line
|
||
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
|
||
|
|
f.write(sudo_stdin)
|
||
|
|
pass_file = f.name
|
||
|
|
os.chmod(pass_file, 0o600)
|
||
|
|
exec_command = f"cat {pass_file} | {exec_command}"
|
||
|
|
# Clean up after execution
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 5: Connection-level URL validation
|
||
|
|
**File:** `tools/url_safety.py`
|
||
|
|
**Lines:** 50-96
|
||
|
|
**CVSS:** 9.4
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER - Add to is_safe_url()
|
||
|
|
# After DNS resolution, verify IP is not in private range
|
||
|
|
def _validate_connection_ip(hostname: str) -> bool:
|
||
|
|
try:
|
||
|
|
addr = socket.getaddrinfo(hostname, None)
|
||
|
|
for a in addr:
|
||
|
|
ip = ipaddress.ip_address(a[4][0])
|
||
|
|
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
||
|
|
return False
|
||
|
|
return True
|
||
|
|
except:
|
||
|
|
return False
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## HIGH PRIORITY FIXES
|
||
|
|
|
||
|
|
### Fix 6: MCP OAuth token validation
|
||
|
|
**File:** `tools/mcp_oauth.py`
|
||
|
|
**Lines:** 66-89
|
||
|
|
**CVSS:** 8.8
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
async def get_tokens(self):
|
||
|
|
data = self._read_json(self._tokens_path())
|
||
|
|
if not data:
|
||
|
|
return None
|
||
|
|
# Add schema validation
|
||
|
|
if not self._validate_token_schema(data):
|
||
|
|
logger.error("Invalid token schema, deleting corrupted tokens")
|
||
|
|
self.remove()
|
||
|
|
return None
|
||
|
|
return OAuthToken(**data)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 7: API Server SQL injection prevention
|
||
|
|
**File:** `gateway/platforms/api_server.py`
|
||
|
|
**Lines:** 98-126
|
||
|
|
**CVSS:** 8.5
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
def _validate_response_id(self, response_id: str) -> bool:
|
||
|
|
"""Validate response_id format to prevent injection."""
|
||
|
|
try:
|
||
|
|
uuid.UUID(response_id.split('-')[0], version=4)
|
||
|
|
return True
|
||
|
|
except (ValueError, IndexError):
|
||
|
|
return False
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 8: CORS strict validation
|
||
|
|
**File:** `gateway/platforms/api_server.py`
|
||
|
|
**Lines:** 324-328
|
||
|
|
**CVSS:** 8.2
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
if "*" in self._cors_origins:
|
||
|
|
logger.error("Wildcard CORS not allowed with credentials")
|
||
|
|
return None # Reject wildcard with credentials
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 9: Require explicit API key
|
||
|
|
**File:** `gateway/platforms/api_server.py`
|
||
|
|
**Lines:** 360-361
|
||
|
|
**CVSS:** 8.1
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
if not self._api_key:
|
||
|
|
logger.error("API server started without authentication")
|
||
|
|
return web.json_response(
|
||
|
|
{"error": "Server authentication not configured"},
|
||
|
|
status=500
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 10: CDP URL validation
|
||
|
|
**File:** `tools/browser_tool.py`
|
||
|
|
**Lines:** 195-208
|
||
|
|
**CVSS:** 8.4
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
def _resolve_cdp_override(self, cdp_url: str) -> str:
|
||
|
|
parsed = urlparse(cdp_url)
|
||
|
|
if parsed.scheme not in ('ws', 'wss', 'http', 'https'):
|
||
|
|
raise ValueError("Invalid CDP scheme")
|
||
|
|
if parsed.hostname not in self._allowed_cdp_hosts:
|
||
|
|
raise ValueError("CDP host not in allowlist")
|
||
|
|
return cdp_url
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 11: Skills guard normalization
|
||
|
|
**File:** `tools/skills_guard.py`
|
||
|
|
**Lines:** 82-484
|
||
|
|
**CVSS:** 7.8
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER - Add to scan_skill()
|
||
|
|
def normalize_for_scanning(content: str) -> str:
|
||
|
|
"""Normalize content to detect obfuscated threats."""
|
||
|
|
# Normalize Unicode
|
||
|
|
content = unicodedata.normalize('NFKC', content)
|
||
|
|
# Normalize case
|
||
|
|
content = content.lower()
|
||
|
|
# Remove common obfuscation
|
||
|
|
content = content.replace('\\x', '')
|
||
|
|
content = content.replace('\\u', '')
|
||
|
|
return content
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 12: Docker volume validation
|
||
|
|
**File:** `tools/environments/docker.py`
|
||
|
|
**Line:** 267
|
||
|
|
**CVSS:** 8.7
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
_BLOCKED_PATHS = ['/var/run/docker.sock', '/proc', '/sys', '/dev']
|
||
|
|
for vol in volumes:
|
||
|
|
if any(blocked in vol for blocked in _BLOCKED_PATHS):
|
||
|
|
raise SecurityError(f"Volume mount {vol} blocked")
|
||
|
|
volume_args.extend(["-v", vol])
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 13: Secure error messages
|
||
|
|
**File:** Multiple files
|
||
|
|
**CVSS:** 7.5
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER - Add to all exception handlers
|
||
|
|
try:
|
||
|
|
operation()
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error: {e}", exc_info=True) # Full details for logs
|
||
|
|
raise UserError("Operation failed") # Generic for user
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 14: OAuth state validation
|
||
|
|
**File:** `tools/mcp_oauth.py`
|
||
|
|
**Line:** 186
|
||
|
|
**CVSS:** 7.6
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
code, state = await _wait_for_callback()
|
||
|
|
stored_state = storage.get_state()
|
||
|
|
if not hmac.compare_digest(state, stored_state):
|
||
|
|
raise SecurityError("OAuth state mismatch - possible CSRF")
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 15: File operation race condition fix
|
||
|
|
**File:** `tools/file_operations.py`
|
||
|
|
**CVSS:** 7.4
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
import fcntl
|
||
|
|
|
||
|
|
def safe_file_access(path: Path):
|
||
|
|
fd = os.open(path, os.O_RDONLY)
|
||
|
|
try:
|
||
|
|
fcntl.flock(fd, fcntl.LOCK_SH)
|
||
|
|
# Perform operations on fd, not path
|
||
|
|
return os.read(fd, size)
|
||
|
|
finally:
|
||
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
||
|
|
os.close(fd)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 16: Add rate limiting
|
||
|
|
**File:** `gateway/platforms/api_server.py`
|
||
|
|
**CVSS:** 7.3
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER - Add middleware
|
||
|
|
from aiohttp_limiter import Limiter
|
||
|
|
|
||
|
|
limiter = Limiter(
|
||
|
|
rate=100, # requests
|
||
|
|
per=60, # per minute
|
||
|
|
key_func=lambda req: req.remote
|
||
|
|
)
|
||
|
|
|
||
|
|
@app.middleware
|
||
|
|
async def rate_limit_middleware(request, handler):
|
||
|
|
if not limiter.is_allowed(request):
|
||
|
|
return web.json_response(
|
||
|
|
{"error": "Rate limit exceeded"},
|
||
|
|
status=429
|
||
|
|
)
|
||
|
|
return await handler(request)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 17: Secure temp file creation
|
||
|
|
**File:** `tools/code_execution_tool.py`
|
||
|
|
**Line:** 388
|
||
|
|
**CVSS:** 7.2
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
import tempfile
|
||
|
|
import os
|
||
|
|
|
||
|
|
fd, tmpdir = tempfile.mkstemp(prefix="hermes_sandbox_", suffix=".tmp")
|
||
|
|
os.chmod(tmpdir, 0o700) # Owner only
|
||
|
|
os.close(fd)
|
||
|
|
# Use tmpdir securely
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## MEDIUM PRIORITY FIXES
|
||
|
|
|
||
|
|
### Fix 18: Expand dangerous patterns
|
||
|
|
**File:** `tools/approval.py`
|
||
|
|
**Lines:** 40-78
|
||
|
|
**CVSS:** 6.5
|
||
|
|
|
||
|
|
Add patterns:
|
||
|
|
```python
|
||
|
|
(r'\bcurl\s+.*\|\s*sh\b', "pipe remote content to shell"),
|
||
|
|
(r'\bwget\s+.*\|\s*bash\b', "pipe remote content to shell"),
|
||
|
|
(r'python\s+-c\s+.*import\s+os', "python os import"),
|
||
|
|
(r'perl\s+-e\s+.*system', "perl system call"),
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 19: Credential file permissions
|
||
|
|
**File:** `tools/credential_files.py`, `tools/mcp_oauth.py`
|
||
|
|
**CVSS:** 6.4
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
def _write_json(path: Path, data: dict) -> None:
|
||
|
|
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||
|
|
path.chmod(0o600)
|
||
|
|
# Verify permissions were set
|
||
|
|
stat = path.stat()
|
||
|
|
if stat.st_mode & 0o077:
|
||
|
|
raise SecurityError("Failed to set restrictive permissions")
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 20: Log sanitization
|
||
|
|
**File:** Multiple logging statements
|
||
|
|
**CVSS:** 5.8
|
||
|
|
|
||
|
|
```python
|
||
|
|
# AFTER
|
||
|
|
from agent.redact import redact_sensitive_text
|
||
|
|
|
||
|
|
# In all logging calls
|
||
|
|
logger.info(redact_sensitive_text(f"Processing {user_input}"))
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ADDITIONAL FIXES (21-32)
|
||
|
|
|
||
|
|
### Fix 21: XXE Prevention
|
||
|
|
**File:** PowerPoint XML processing
|
||
|
|
Add:
|
||
|
|
```python
|
||
|
|
from defusedxml import ElementTree as ET
|
||
|
|
# Use defusedxml instead of standard xml
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 22: YAML Safe Loading Audit
|
||
|
|
**File:** `hermes_cli/config.py`
|
||
|
|
Audit all yaml.safe_load calls for custom constructors.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 23: Prototype Pollution Fix
|
||
|
|
**File:** `scripts/whatsapp-bridge/bridge.js`
|
||
|
|
Use Map instead of Object for user-controlled keys.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 24: Subagent Isolation
|
||
|
|
**File:** `tools/delegate_tool.py`
|
||
|
|
Implement filesystem namespace isolation.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 25: Secure Session IDs
|
||
|
|
**File:** `gateway/session.py`
|
||
|
|
Use secrets.token_urlsafe(32) instead of uuid4.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 26: Binary Integrity Checks
|
||
|
|
**File:** `tools/tirith_security.py`
|
||
|
|
Require GPG signature verification.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 27: Debug Output Redaction
|
||
|
|
**File:** `tools/debug_helpers.py`
|
||
|
|
Apply redact_sensitive_text to all debug output.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 28: Security Headers
|
||
|
|
**File:** `gateway/platforms/api_server.py`
|
||
|
|
Add:
|
||
|
|
```python
|
||
|
|
"Content-Security-Policy": "default-src 'self'",
|
||
|
|
"Strict-Transport-Security": "max-age=31536000",
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 29: Version Information Minimization
|
||
|
|
**File:** Version endpoints
|
||
|
|
Return minimal version information publicly.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 30: Dead Code Removal
|
||
|
|
**File:** Multiple
|
||
|
|
Remove unused imports and functions.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 31: Token Encryption at Rest
|
||
|
|
**File:** `hermes_cli/auth.py`
|
||
|
|
Use OS keychain or encrypt auth.json.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Fix 32: Input Length Validation
|
||
|
|
**File:** All tool entry points
|
||
|
|
Add MAX_INPUT_LENGTH checks everywhere.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## IMPLEMENTATION VERIFICATION
|
||
|
|
|
||
|
|
### Testing Requirements
|
||
|
|
- [ ] All fixes have unit tests
|
||
|
|
- [ ] Security regression tests pass
|
||
|
|
- [ ] Fuzzing shows no new vulnerabilities
|
||
|
|
- [ ] Penetration test completed
|
||
|
|
- [ ] Code review by security team
|
||
|
|
|
||
|
|
### Sign-off Required
|
||
|
|
- [ ] Security Team Lead
|
||
|
|
- [ ] Engineering Manager
|
||
|
|
- [ ] QA Lead
|
||
|
|
- [ ] DevOps Lead
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Last Updated:** March 30, 2026
|
||
|
|
**Next Review:** After all P0/P1 fixes completed
|