# SECURE CODING GUIDELINES ## Hermes Agent Development Security Standards **Version:** 1.0 **Effective Date:** March 30, 2026 --- ## 1. GENERAL PRINCIPLES ### 1.1 Security-First Mindset - Every feature must be designed with security in mind - Assume all input is malicious until proven otherwise - Defense in depth: multiple layers of security controls - Fail securely: when security controls fail, default to denial ### 1.2 Threat Model Primary threats to consider: - Malicious user prompts - Compromised or malicious skills - Supply chain attacks - Insider threats - Accidental data exposure --- ## 2. INPUT VALIDATION ### 2.1 Validate All Input ```python # ❌ INCORRECT def process_file(path: str): with open(path) as f: return f.read() # ✅ CORRECT from pydantic import BaseModel, validator import re class FileRequest(BaseModel): path: str max_size: int = 1000000 @validator('path') def validate_path(cls, v): # Block path traversal if '..' in v or v.startswith('/'): raise ValueError('Invalid path characters') # Allowlist safe characters if not re.match(r'^[\w\-./]+$', v): raise ValueError('Invalid characters in path') return v @validator('max_size') def validate_size(cls, v): if v < 0 or v > 10000000: raise ValueError('Size out of range') return v def process_file(request: FileRequest): # Now safe to use request.path pass ``` ### 2.2 Length Limits Always enforce maximum lengths: ```python MAX_INPUT_LENGTH = 10000 MAX_FILENAME_LENGTH = 255 MAX_PATH_LENGTH = 4096 def validate_length(value: str, max_len: int, field_name: str): if len(value) > max_len: raise ValueError(f"{field_name} exceeds maximum length of {max_len}") ``` ### 2.3 Type Safety Use type hints and enforce them: ```python from typing import Union def safe_function(user_id: int, message: str) -> dict: if not isinstance(user_id, int): raise TypeError("user_id must be an integer") if not isinstance(message, str): raise TypeError("message must be a string") # ... function logic ``` --- ## 3. COMMAND EXECUTION ### 3.1 Never Use shell=True ```python import subprocess import shlex # ❌ NEVER DO THIS subprocess.run(f"ls {user_input}", shell=True) # ❌ NEVER DO THIS EITHER cmd = f"cat {filename}" os.system(cmd) # ✅ CORRECT - Use list arguments subprocess.run(["ls", user_input], shell=False) # ✅ CORRECT - Use shlex for complex cases cmd_parts = shlex.split(user_input) subprocess.run(["ls"] + cmd_parts, shell=False) ``` ### 3.2 Command Allowlisting ```python ALLOWED_COMMANDS = frozenset([ "ls", "cat", "grep", "find", "git", "python", "pip" ]) def validate_command(command: str): parts = shlex.split(command) if parts[0] not in ALLOWED_COMMANDS: raise SecurityError(f"Command '{parts[0]}' not allowed") ``` ### 3.3 Input Sanitization ```python import re def sanitize_shell_input(value: str) -> str: """Remove dangerous shell metacharacters.""" # Block shell metacharacters dangerous = re.compile(r'[;&|`$(){}[\]\\]') if dangerous.search(value): raise ValueError("Shell metacharacters not allowed") return value ``` --- ## 4. FILE OPERATIONS ### 4.1 Path Validation ```python from pathlib import Path class FileSandbox: def __init__(self, root: Path): self.root = root.resolve() def validate_path(self, user_path: str) -> Path: """Validate and resolve user-provided path within sandbox.""" # Expand user home expanded = Path(user_path).expanduser() # Resolve to absolute path try: resolved = expanded.resolve() except (OSError, ValueError) as e: raise SecurityError(f"Invalid path: {e}") # Ensure path is within sandbox try: resolved.relative_to(self.root) except ValueError: raise SecurityError("Path outside sandbox") return resolved def safe_open(self, user_path: str, mode: str = 'r'): safe_path = self.validate_path(user_path) return open(safe_path, mode) ``` ### 4.2 Prevent Symlink Attacks ```python import os def safe_read_file(filepath: Path): """Read file, following symlinks only within allowed directories.""" # Resolve symlinks real_path = filepath.resolve() # Verify still in allowed location after resolution if not str(real_path).startswith(str(SAFE_ROOT)): raise SecurityError("Symlink escape detected") # Verify it's a regular file if not real_path.is_file(): raise SecurityError("Not a regular file") return real_path.read_text() ``` ### 4.3 Temporary Files ```python import tempfile import os def create_secure_temp_file(): """Create temp file with restricted permissions.""" # Create with restrictive permissions fd, path = tempfile.mkstemp(prefix="hermes_", suffix=".tmp") try: # Set owner-read/write only os.chmod(path, 0o600) return fd, path except: os.close(fd) os.unlink(path) raise ``` --- ## 5. SECRET MANAGEMENT ### 5.1 Environment Variables ```python import os # ❌ NEVER DO THIS def execute_command(command: str): # Child inherits ALL environment subprocess.run(command, shell=True, env=os.environ) # ✅ CORRECT - Explicit whitelisting _ALLOWED_ENV = frozenset([ "PATH", "HOME", "USER", "LANG", "TERM", "SHELL" ]) def get_safe_environment(): return {k: v for k, v in os.environ.items() if k in _ALLOWED_ENV} def execute_command(command: str): subprocess.run( command, shell=False, env=get_safe_environment() ) ``` ### 5.2 Secret Detection ```python import re _SECRET_PATTERNS = [ re.compile(r'sk-[a-zA-Z0-9]{20,}'), # OpenAI-style keys re.compile(r'ghp_[a-zA-Z0-9]{36}'), # GitHub PAT re.compile(r'[a-zA-Z0-9]{40}'), # Generic high-entropy strings ] def detect_secrets(text: str) -> list: """Detect potential secrets in text.""" findings = [] for pattern in _SECRET_PATTERNS: matches = pattern.findall(text) findings.extend(matches) return findings def redact_secrets(text: str) -> str: """Redact detected secrets.""" for pattern in _SECRET_PATTERNS: text = pattern.sub('***REDACTED***', text) return text ``` ### 5.3 Secure Logging ```python import logging from agent.redact import redact_sensitive_text class SecureLogger: def __init__(self, logger: logging.Logger): self.logger = logger def debug(self, msg: str, *args, **kwargs): self.logger.debug(redact_sensitive_text(msg), *args, **kwargs) def info(self, msg: str, *args, **kwargs): self.logger.info(redact_sensitive_text(msg), *args, **kwargs) def warning(self, msg: str, *args, **kwargs): self.logger.warning(redact_sensitive_text(msg), *args, **kwargs) def error(self, msg: str, *args, **kwargs): self.logger.error(redact_sensitive_text(msg), *args, **kwargs) ``` --- ## 6. NETWORK SECURITY ### 6.1 URL Validation ```python from urllib.parse import urlparse import ipaddress _BLOCKED_SCHEMES = frozenset(['file', 'ftp', 'gopher']) _BLOCKED_HOSTS = frozenset([ 'localhost', '127.0.0.1', '0.0.0.0', '169.254.169.254', # AWS metadata '[::1]', '[::]' ]) _PRIVATE_NETWORKS = [ ipaddress.ip_network('10.0.0.0/8'), ipaddress.ip_network('172.16.0.0/12'), ipaddress.ip_network('192.168.0.0/16'), ipaddress.ip_network('127.0.0.0/8'), ipaddress.ip_network('169.254.0.0/16'), # Link-local ] def validate_url(url: str) -> bool: """Validate URL is safe to fetch.""" parsed = urlparse(url) # Check scheme if parsed.scheme not in ('http', 'https'): raise ValueError(f"Scheme '{parsed.scheme}' not allowed") # Check hostname hostname = parsed.hostname if not hostname: raise ValueError("No hostname in URL") if hostname.lower() in _BLOCKED_HOSTS: raise ValueError("Host not allowed") # Check IP addresses try: ip = ipaddress.ip_address(hostname) for network in _PRIVATE_NETWORKS: if ip in network: raise ValueError("Private IP address not allowed") except ValueError: pass # Not an IP, continue return True ``` ### 6.2 Redirect Handling ```python import requests def safe_get(url: str, max_redirects: int = 5): """GET URL with redirect validation.""" session = requests.Session() session.max_redirects = max_redirects # Validate initial URL validate_url(url) # Custom redirect handler response = session.get( url, allow_redirects=True, hooks={'response': lambda r, *args, **kwargs: validate_url(r.url)} ) return response ``` --- ## 7. AUTHENTICATION & AUTHORIZATION ### 7.1 API Key Validation ```python import secrets import hmac import hashlib def constant_time_compare(val1: str, val2: str) -> bool: """Compare strings in constant time to prevent timing attacks.""" return hmac.compare_digest(val1.encode(), val2.encode()) def validate_api_key(provided_key: str, expected_key: str) -> bool: """Validate API key using constant-time comparison.""" if not provided_key or not expected_key: return False return constant_time_compare(provided_key, expected_key) ``` ### 7.2 Session Management ```python import secrets from datetime import datetime, timedelta class SessionManager: SESSION_TIMEOUT = timedelta(hours=24) def create_session(self, user_id: str) -> str: """Create secure session token.""" token = secrets.token_urlsafe(32) expires = datetime.utcnow() + self.SESSION_TIMEOUT # Store in database with expiration return token def validate_session(self, token: str) -> bool: """Validate session token.""" # Lookup in database # Check expiration # Validate token format return True ``` --- ## 8. ERROR HANDLING ### 8.1 Secure Error Messages ```python import logging # Internal detailed logging logger = logging.getLogger(__name__) class UserFacingError(Exception): """Error safe to show to users.""" pass def process_request(data: dict): try: result = internal_operation(data) return result except ValueError as e: # Log full details internally logger.error(f"Validation error: {e}", exc_info=True) # Return safe message to user raise UserFacingError("Invalid input provided") except Exception as e: # Log full details internally logger.error(f"Unexpected error: {e}", exc_info=True) # Generic message to user raise UserFacingError("An error occurred") ``` ### 8.2 Exception Handling ```python def safe_operation(): try: risky_operation() except Exception as e: # Always clean up resources cleanup_resources() # Log securely logger.error(f"Operation failed: {redact_sensitive_text(str(e))}") # Re-raise or convert raise ``` --- ## 9. CRYPTOGRAPHY ### 9.1 Password Hashing ```python import bcrypt def hash_password(password: str) -> str: """Hash password using bcrypt.""" salt = bcrypt.gensalt(rounds=12) hashed = bcrypt.hashpw(password.encode(), salt) return hashed.decode() def verify_password(password: str, hashed: str) -> bool: """Verify password against hash.""" return bcrypt.checkpw(password.encode(), hashed.encode()) ``` ### 9.2 Secure Random ```python import secrets def generate_token(length: int = 32) -> str: """Generate cryptographically secure token.""" return secrets.token_urlsafe(length) def generate_pin(length: int = 6) -> str: """Generate secure numeric PIN.""" return ''.join(str(secrets.randbelow(10)) for _ in range(length)) ``` --- ## 10. CODE REVIEW CHECKLIST ### Before Submitting Code: - [ ] All user inputs validated - [ ] No shell=True in subprocess calls - [ ] All file paths validated and sandboxed - [ ] Secrets not logged or exposed - [ ] URLs validated before fetching - [ ] Error messages don't leak sensitive info - [ ] No hardcoded credentials - [ ] Proper exception handling - [ ] Security tests included - [ ] Documentation updated ### Security-Focused Review Questions: 1. What happens if this receives malicious input? 2. Can this leak sensitive data? 3. Are there privilege escalation paths? 4. What if the external service is compromised? 5. Is the error handling secure? --- ## 11. TESTING SECURITY ### 11.1 Security Unit Tests ```python def test_path_traversal_blocked(): sandbox = FileSandbox(Path("/safe/path")) with pytest.raises(SecurityError): sandbox.validate_path("../../../etc/passwd") def test_command_injection_blocked(): with pytest.raises(SecurityError): validate_command("ls; rm -rf /") def test_secret_redaction(): text = "Key: sk-test123456789" redacted = redact_secrets(text) assert "sk-test" not in redacted ``` ### 11.2 Fuzzing ```python import hypothesis.strategies as st from hypothesis import given @given(st.text()) def test_input_validation(input_text): # Should never crash, always validate or reject try: result = process_input(input_text) assert isinstance(result, ExpectedType) except ValidationError: pass # Expected for invalid input ``` --- ## 12. INCIDENT RESPONSE ### Security Incident Procedure: 1. **Stop** - Halt the affected system/process 2. **Assess** - Determine scope and impact 3. **Contain** - Prevent further damage 4. **Investigate** - Gather evidence 5. **Remediate** - Fix the vulnerability 6. **Recover** - Restore normal operations 7. **Learn** - Document and improve ### Emergency Contacts: - Security Team: security@example.com - On-call: +1-XXX-XXX-XXXX - Slack: #security-incidents --- **Document Owner:** Security Team **Review Cycle:** Quarterly **Last Updated:** March 30, 2026