feat: extract sovereign work from hermes-agent fork into sidecar
Extracted 52 files from Timmy_Foundation/hermes-agent (gitea/main) into hermes-sovereign/ directory to restore clean upstream tracking. Layout: docs/ 19 files — deploy guides, performance reports, security docs, research security/ 5 files — audit workflows, PR checklists, validation scripts wizard-bootstrap/ 7 files — wizard environment, dependency checking, auditing notebooks/ 2 files — Jupyter health monitoring notebooks scripts/ 5 files — forge health, smoke tests, syntax guard, deploy validation ci/ 2 files — Gitea CI workflow definitions githooks/ 3 files — pre-commit hooks and config devkit/ 8 files — developer toolkit (Gitea client, health, notebook runner) README.md 1 file — directory overview Addresses: #337, #338
This commit is contained in:
488
hermes-sovereign/docs/SECURITY_FIXES_CHECKLIST.md
Normal file
488
hermes-sovereign/docs/SECURITY_FIXES_CHECKLIST.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user