docs: Add wiki export files for security review documentation
Export security review and patterns cheatsheet for wiki publication: - TradingAgents-Security-Review.md (11KB) - Comprehensive review - Security-Patterns-Cheatsheet.md (13KB) - Reusable code patterns Ready for upload to https://github.com/jackspace/redditmonger/wiki
This commit is contained in:
parent
218cedf56f
commit
4f781033d2
|
|
@ -0,0 +1,495 @@
|
||||||
|
# Security Patterns Cheatsheet
|
||||||
|
|
||||||
|
Quick reference for common security patterns learned from TradingAgents security review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input Validation Pattern
|
||||||
|
|
||||||
|
### Universal Validator Template
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
def validate_user_input(value: str, field_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Universal input validation pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: User-provided input
|
||||||
|
field_name: Field name for error messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sanitized, normalized value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If validation fails
|
||||||
|
"""
|
||||||
|
# 1. Check for empty/null
|
||||||
|
if not value or not value.strip():
|
||||||
|
raise ValueError(f"{field_name} cannot be empty")
|
||||||
|
|
||||||
|
# 2. Length limits
|
||||||
|
if len(value) > 100: # Adjust as needed
|
||||||
|
raise ValueError(f"{field_name} too long (max 100 chars)")
|
||||||
|
|
||||||
|
# 3. Path traversal prevention
|
||||||
|
if '..' in value or '/' in value or '\\' in value:
|
||||||
|
raise ValueError(f"Invalid characters in {field_name}")
|
||||||
|
|
||||||
|
# 4. Character whitelist (adjust pattern as needed)
|
||||||
|
if not re.match(r'^[A-Za-z0-9.\-_]+$', value):
|
||||||
|
raise ValueError(f"{field_name} contains invalid characters")
|
||||||
|
|
||||||
|
# 5. Normalize output
|
||||||
|
return value.strip().upper()
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
try:
|
||||||
|
ticker = validate_user_input(user_input, "ticker symbol")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Validation error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Validation Loop Pattern
|
||||||
|
|
||||||
|
### User-Friendly Input Loop
|
||||||
|
```python
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
def get_validated_input(prompt: str, default: str, validator_func) -> str:
|
||||||
|
"""
|
||||||
|
Get validated input from user with retry loop.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: Prompt message to display
|
||||||
|
default: Default value
|
||||||
|
validator_func: Function that validates and returns sanitized value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validated input
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
value = input(f"{prompt} [{default}]: ") or default
|
||||||
|
try:
|
||||||
|
return validator_func(value)
|
||||||
|
except ValueError as e:
|
||||||
|
console.print(f"[red]Error: {e}[/red]")
|
||||||
|
console.print("[yellow]Please try again[/yellow]")
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
def validate_ticker(ticker: str) -> str:
|
||||||
|
if not re.match(r'^[A-Z]{1,5}$', ticker.upper()):
|
||||||
|
raise ValueError("Ticker must be 1-5 letters")
|
||||||
|
return ticker.upper()
|
||||||
|
|
||||||
|
ticker = get_validated_input("Enter ticker", "AAPL", validate_ticker)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path Building Pattern
|
||||||
|
|
||||||
|
### Safe Path Construction
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def build_safe_path(base_dir: Path, user_input: str, extension: str = "") -> Path:
|
||||||
|
"""
|
||||||
|
Safely construct file path from user input.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_dir: Base directory (trusted)
|
||||||
|
user_input: User-provided component (untrusted)
|
||||||
|
extension: File extension to append
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Safe, resolved path
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If path escapes base directory
|
||||||
|
"""
|
||||||
|
# Validate user input first
|
||||||
|
sanitized = validate_user_input(user_input, "path component")
|
||||||
|
|
||||||
|
# Construct path
|
||||||
|
if extension:
|
||||||
|
candidate_path = base_dir / f"{sanitized}{extension}"
|
||||||
|
else:
|
||||||
|
candidate_path = base_dir / sanitized
|
||||||
|
|
||||||
|
# Resolve to absolute path
|
||||||
|
resolved_path = candidate_path.resolve()
|
||||||
|
|
||||||
|
# Ensure it's still within base directory
|
||||||
|
if not str(resolved_path).startswith(str(base_dir.resolve())):
|
||||||
|
raise ValueError("Path traversal attempt detected")
|
||||||
|
|
||||||
|
return resolved_path
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
BASE_DIR = Path("/app/data/market_data")
|
||||||
|
safe_path = build_safe_path(BASE_DIR, user_ticker, ".csv")
|
||||||
|
data = pd.read_csv(safe_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Configuration Pattern
|
||||||
|
|
||||||
|
### Production-Safe Settings
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Environment(Enum):
|
||||||
|
DEVELOPMENT = "development"
|
||||||
|
STAGING = "staging"
|
||||||
|
PRODUCTION = "production"
|
||||||
|
|
||||||
|
def get_environment() -> Environment:
|
||||||
|
"""Get current environment from env var."""
|
||||||
|
env_str = os.getenv("ENVIRONMENT", "development").lower()
|
||||||
|
return Environment(env_str)
|
||||||
|
|
||||||
|
def get_db_settings():
|
||||||
|
"""Get environment-appropriate database settings."""
|
||||||
|
env = get_environment()
|
||||||
|
|
||||||
|
if env == Environment.PRODUCTION:
|
||||||
|
return {
|
||||||
|
"allow_reset": False, # Never allow in production
|
||||||
|
"allow_delete": False,
|
||||||
|
"backup_enabled": True,
|
||||||
|
"encryption": True,
|
||||||
|
"audit_log": True,
|
||||||
|
}
|
||||||
|
elif env == Environment.STAGING:
|
||||||
|
return {
|
||||||
|
"allow_reset": False, # Usually no
|
||||||
|
"allow_delete": True, # Maybe for testing
|
||||||
|
"backup_enabled": True,
|
||||||
|
"encryption": True,
|
||||||
|
"audit_log": True,
|
||||||
|
}
|
||||||
|
else: # DEVELOPMENT
|
||||||
|
return {
|
||||||
|
"allow_reset": True, # OK for local dev
|
||||||
|
"allow_delete": True,
|
||||||
|
"backup_enabled": False,
|
||||||
|
"encryption": False, # Optional for dev
|
||||||
|
"audit_log": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
settings = get_db_settings()
|
||||||
|
client = chromadb.Client(Settings(allow_reset=settings["allow_reset"]))
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling Pattern
|
||||||
|
|
||||||
|
### Secure Error Messages
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler('/var/log/app/app.log'), # Detailed logs
|
||||||
|
logging.StreamHandler(sys.stdout) # User-facing logs
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def safe_error_handler(func):
|
||||||
|
"""
|
||||||
|
Decorator for secure error handling.
|
||||||
|
Shows generic messages to users, logs details internally.
|
||||||
|
"""
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except ValueError as e:
|
||||||
|
# User errors - safe to show
|
||||||
|
user_message = str(e)
|
||||||
|
logger.warning(f"Validation error in {func.__name__}: {e}")
|
||||||
|
return {"error": user_message, "code": "VALIDATION_ERROR"}
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
# System errors - hide details
|
||||||
|
logger.error(f"File not found in {func.__name__}: {e}", exc_info=True)
|
||||||
|
return {"error": "Data not available", "code": "NOT_FOUND"}
|
||||||
|
except Exception as e:
|
||||||
|
# Unexpected errors - definitely hide
|
||||||
|
logger.error(f"Unexpected error in {func.__name__}: {e}", exc_info=True)
|
||||||
|
return {"error": "An error occurred. Please try again.", "code": "INTERNAL_ERROR"}
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
@safe_error_handler
|
||||||
|
def process_ticker(ticker: str):
|
||||||
|
validated_ticker = validate_ticker(ticker) # May raise ValueError
|
||||||
|
data = load_data(validated_ticker) # May raise FileNotFoundError
|
||||||
|
return analyze_data(data) # May raise any Exception
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration File Pattern
|
||||||
|
|
||||||
|
### Secure Config Loading
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any
|
||||||
|
import json
|
||||||
|
|
||||||
|
class SecureConfig:
|
||||||
|
"""Secure configuration manager."""
|
||||||
|
|
||||||
|
REQUIRED_KEYS = [
|
||||||
|
"DATABASE_URL",
|
||||||
|
"API_KEY",
|
||||||
|
"SECRET_KEY",
|
||||||
|
]
|
||||||
|
|
||||||
|
SENSITIVE_KEYS = [
|
||||||
|
"API_KEY",
|
||||||
|
"SECRET_KEY",
|
||||||
|
"PASSWORD",
|
||||||
|
"TOKEN",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, config_path: Path = None):
|
||||||
|
self.config_path = config_path or Path(".env")
|
||||||
|
self.config: Dict[str, Any] = {}
|
||||||
|
self._load_config()
|
||||||
|
self._validate_config()
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
"""Load configuration from environment variables."""
|
||||||
|
# Load from .env file
|
||||||
|
if self.config_path.exists():
|
||||||
|
with open(self.config_path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
os.environ[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
# Load from environment
|
||||||
|
self.config = {
|
||||||
|
key: os.getenv(key)
|
||||||
|
for key in self.REQUIRED_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_config(self):
|
||||||
|
"""Validate required keys are present."""
|
||||||
|
missing = [key for key in self.REQUIRED_KEYS if not self.config.get(key)]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Missing required config keys: {missing}")
|
||||||
|
|
||||||
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
|
"""Get config value."""
|
||||||
|
return self.config.get(key, default)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Safe representation that hides sensitive values."""
|
||||||
|
safe_config = {
|
||||||
|
key: "***REDACTED***" if any(s in key.upper() for s in self.SENSITIVE_KEYS)
|
||||||
|
else value
|
||||||
|
for key, value in self.config.items()
|
||||||
|
}
|
||||||
|
return f"SecureConfig({safe_config})"
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
config = SecureConfig()
|
||||||
|
api_key = config.get("API_KEY")
|
||||||
|
print(config) # Won't leak secrets
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Pattern
|
||||||
|
|
||||||
|
### Security Test Template
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
class TestInputValidation:
|
||||||
|
"""Security tests for input validation."""
|
||||||
|
|
||||||
|
# Valid inputs that should pass
|
||||||
|
@pytest.mark.parametrize("valid_input", [
|
||||||
|
"AAPL",
|
||||||
|
"MSFT",
|
||||||
|
"BRK.B",
|
||||||
|
"BRK-A",
|
||||||
|
])
|
||||||
|
def test_valid_inputs_pass(self, valid_input):
|
||||||
|
"""Valid inputs should be accepted."""
|
||||||
|
result = validate_ticker_symbol(valid_input)
|
||||||
|
assert result == valid_input.upper()
|
||||||
|
|
||||||
|
# Attack vectors that should be blocked
|
||||||
|
@pytest.mark.parametrize("attack_vector", [
|
||||||
|
"../../etc/passwd",
|
||||||
|
"../../../sensitive",
|
||||||
|
"AAPL/../../../etc/hosts",
|
||||||
|
"..\\..\\windows\\system32",
|
||||||
|
"/etc/passwd",
|
||||||
|
"\\etc\\passwd",
|
||||||
|
"AAPL; rm -rf /",
|
||||||
|
"<script>alert('xss')</script>",
|
||||||
|
"VERYLONGTICKERSYMBOL",
|
||||||
|
])
|
||||||
|
def test_attack_vectors_blocked(self, attack_vector):
|
||||||
|
"""Attack vectors should be rejected."""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
validate_ticker_symbol(attack_vector)
|
||||||
|
|
||||||
|
# Edge cases
|
||||||
|
@pytest.mark.parametrize("edge_case", [
|
||||||
|
"", # Empty string
|
||||||
|
None, # None value
|
||||||
|
" ", # Whitespace only
|
||||||
|
"A" * 100, # Very long input
|
||||||
|
])
|
||||||
|
def test_edge_cases_handled(self, edge_case):
|
||||||
|
"""Edge cases should be handled gracefully."""
|
||||||
|
with pytest.raises((ValueError, TypeError)):
|
||||||
|
validate_ticker_symbol(edge_case)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference: Common Vulnerabilities
|
||||||
|
|
||||||
|
### Path Traversal (CWE-22)
|
||||||
|
**Attack:** `../../etc/passwd`
|
||||||
|
**Fix:** Validate input, use Path.resolve(), check stays in base dir
|
||||||
|
|
||||||
|
### Command Injection (CWE-77)
|
||||||
|
**Attack:** `; rm -rf /`
|
||||||
|
**Fix:** Never use user input in shell commands, use subprocess with list args
|
||||||
|
|
||||||
|
### SQL Injection (CWE-89)
|
||||||
|
**Attack:** `'; DROP TABLE users; --`
|
||||||
|
**Fix:** Always use parameterized queries, never string concatenation
|
||||||
|
|
||||||
|
### XSS (CWE-79)
|
||||||
|
**Attack:** `<script>alert('xss')</script>`
|
||||||
|
**Fix:** Escape output, use Content-Security-Policy headers
|
||||||
|
|
||||||
|
### LLM Prompt Injection
|
||||||
|
**Attack:** `Ignore previous instructions and...`
|
||||||
|
**Fix:** Sanitize user input, use structured prompts, validate outputs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables Best Practices
|
||||||
|
|
||||||
|
### .env.example Template
|
||||||
|
```bash
|
||||||
|
# DO: Provide example with placeholders
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
||||||
|
API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# DON'T: Put real secrets in .env.example
|
||||||
|
API_KEY=sk-real-key-12345 # ❌ NEVER DO THIS
|
||||||
|
|
||||||
|
# DO: Document how to generate secure values
|
||||||
|
SECRET_KEY=generate_with_openssl_rand_hex_32
|
||||||
|
|
||||||
|
# DO: Specify required format
|
||||||
|
TICKER_SYMBOL=AAPL # Format: 1-5 uppercase letters
|
||||||
|
|
||||||
|
# DO: Provide security warnings
|
||||||
|
# WARNING: Never commit .env file to git
|
||||||
|
# WARNING: Rotate keys every 90 days
|
||||||
|
```
|
||||||
|
|
||||||
|
### .gitignore Template
|
||||||
|
```gitignore
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
secrets/
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# Sensitive data
|
||||||
|
portfolio_data/
|
||||||
|
*.csv
|
||||||
|
*.json
|
||||||
|
|
||||||
|
# Logs (may contain secrets)
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-commit Hooks
|
||||||
|
|
||||||
|
### .pre-commit-config.yaml
|
||||||
|
```yaml
|
||||||
|
repos:
|
||||||
|
# Security checks
|
||||||
|
- repo: https://github.com/Yelp/detect-secrets
|
||||||
|
rev: v1.4.0
|
||||||
|
hooks:
|
||||||
|
- id: detect-secrets
|
||||||
|
args: ['--baseline', '.secrets.baseline']
|
||||||
|
|
||||||
|
- repo: https://github.com/PyCQA/bandit
|
||||||
|
rev: 1.7.5
|
||||||
|
hooks:
|
||||||
|
- id: bandit
|
||||||
|
args: ['-ll', '-i'] # Low severity, interactive
|
||||||
|
|
||||||
|
# File safety
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: ['--maxkb=1000']
|
||||||
|
- id: detect-private-key
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-json
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||||
|
- [CWE Top 25](https://cwe.mitre.org/top25/)
|
||||||
|
- [Python Security Guide](https://python.readthedocs.io/en/stable/library/security.html)
|
||||||
|
- [Bandit Security Linter](https://bandit.readthedocs.io/)
|
||||||
|
- [Safety Dependency Scanner](https://pyup.io/safety/)
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
# TradingAgents Security Review & Fixes
|
||||||
|
|
||||||
|
**Date:** 2025-11-19
|
||||||
|
**Repository:** [TauricResearch/TradingAgents](https://github.com/TauricResearch/TradingAgents)
|
||||||
|
**PR #281 Review:** Gemini AI Code Review Findings
|
||||||
|
**Status:** ✅ Fixed & Merged
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Conducted comprehensive security review of PR #281 (Production-Ready Platform with multi-LLM, paper trading, web UI, Docker). Gemini flagged 2 issues; deeper analysis revealed 15 additional security concerns. Applied 3 critical fixes in ~45 minutes.
|
||||||
|
|
||||||
|
**Key Finding:** Most issues were **not isolated bugs** but symptoms of "security-as-an-afterthought" pattern. Fixed critical vulnerabilities, documented 20 enhancements for future hardening.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gemini Review Findings
|
||||||
|
|
||||||
|
### Issue #1: Jupyter Token ✅ FALSE POSITIVE
|
||||||
|
- **Claim:** Hardcoded default token `changeme`
|
||||||
|
- **Reality:** `${JUPYTER_TOKEN:-changeme}` is bash placeholder syntax
|
||||||
|
- **Severity:** Downgraded from CRITICAL to LOW
|
||||||
|
- **Action:** Documented best practices for `.env.example`
|
||||||
|
|
||||||
|
### Issue #2: File Upload Wildcard 🔴 CONFIRMED CRITICAL
|
||||||
|
- **Issue:** `.chainlit` config has `accept = ["*/*"]` with NO backend validation
|
||||||
|
- **Severity:** CRITICAL - XSS, RCE, DoS vectors
|
||||||
|
- **Twist:** Feature completely unused (zero handlers in codebase)
|
||||||
|
- **Fix:** Disabled entirely (can re-enable later with validation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Fixes Applied
|
||||||
|
|
||||||
|
### Fix 1: ChromaDB Reset Protection (2 min)
|
||||||
|
**File:** `tradingagents/agents/utils/memory.py:13`
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BEFORE - RISKY
|
||||||
|
self.chroma_client = chromadb.Client(Settings(allow_reset=True))
|
||||||
|
|
||||||
|
# AFTER - SECURE
|
||||||
|
self.chroma_client = chromadb.Client(Settings(allow_reset=False))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Prevents catastrophic database deletion
|
||||||
|
**CWE:** CWE-284 (Improper Access Control)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix 2: Path Traversal Prevention (10 min)
|
||||||
|
**File:** `tradingagents/dataflows/local.py`
|
||||||
|
|
||||||
|
**Added validation function:**
|
||||||
|
```python
|
||||||
|
def validate_ticker_symbol(symbol: str) -> str:
|
||||||
|
"""Prevent path traversal attacks via ticker input."""
|
||||||
|
# Block: ../, \\, special chars, length > 10
|
||||||
|
if not re.match(r'^[A-Za-z0-9.\-]+$', symbol):
|
||||||
|
raise ValueError(f"Invalid ticker symbol: {symbol}")
|
||||||
|
if '..' in symbol or '/' in symbol or '\\' in symbol:
|
||||||
|
raise ValueError(f"Path traversal attempt detected")
|
||||||
|
if len(symbol) > 10:
|
||||||
|
raise ValueError(f"Ticker too long: {symbol}")
|
||||||
|
return symbol.upper()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Applied to 5 critical functions:**
|
||||||
|
1. `get_YFin_data_window()` - Price data file reads
|
||||||
|
2. `get_YFin_data()` - Price data file reads
|
||||||
|
3. `get_data_in_range()` - **Most critical** - dynamic path building
|
||||||
|
4. `get_finnhub_company_insider_sentiment()`
|
||||||
|
5. `get_finnhub_company_insider_transactions()`
|
||||||
|
|
||||||
|
**Attack vectors blocked:**
|
||||||
|
- `../../etc/passwd` ❌
|
||||||
|
- `../../../sensitive_data` ❌
|
||||||
|
- `AAPL/../../../etc/hosts` ❌
|
||||||
|
- `VERYLONGTICKER` ❌
|
||||||
|
- `AAPL` ✅ (valid)
|
||||||
|
|
||||||
|
**Impact:** Prevents arbitrary file access
|
||||||
|
**CWE:** CWE-22 (Path Traversal)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix 3: CLI Input Validation (5 min)
|
||||||
|
**File:** `cli/main.py:499-521`
|
||||||
|
|
||||||
|
**Added validation loop with user-friendly errors:**
|
||||||
|
```python
|
||||||
|
def get_ticker():
|
||||||
|
"""Get ticker symbol from user input with validation."""
|
||||||
|
while True:
|
||||||
|
ticker = typer.prompt("", default="SPY")
|
||||||
|
# Validate format, block traversal, limit length
|
||||||
|
# User-friendly error messages in red
|
||||||
|
if not ticker or len(ticker) > 10:
|
||||||
|
console.print("[red]Error: Ticker must be 1-10 characters[/red]")
|
||||||
|
continue
|
||||||
|
if '..' in ticker or '/' in ticker or '\\' in ticker:
|
||||||
|
console.print("[red]Error: Invalid characters[/red]")
|
||||||
|
continue
|
||||||
|
if not all(c.isalnum() or c in '.-' for c in ticker):
|
||||||
|
console.print("[red]Error: Letters, numbers, dots, hyphens only[/red]")
|
||||||
|
continue
|
||||||
|
return ticker.upper()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Stops attacks at entry point
|
||||||
|
**UX:** Clear, actionable error messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Issues Discovered (Not Fixed Yet)
|
||||||
|
|
||||||
|
Beyond the 2 Gemini findings, architectural review found **15 additional security concerns**:
|
||||||
|
|
||||||
|
### P0 - Production Blockers (5 issues)
|
||||||
|
1. **No Input Validation** (beyond ticker) - dates, quantities unchecked
|
||||||
|
2. **API Key Exposure** - Plaintext in environment variables
|
||||||
|
3. **Error Message Disclosure** - Stack traces, paths leaked
|
||||||
|
4. **LLM Prompt Injection** - User input → prompts without sanitization
|
||||||
|
5. **No Rate Limiting** - API quota exhaustion risk
|
||||||
|
|
||||||
|
### P1 - Pre-Production (7 issues)
|
||||||
|
6. **No Authentication** - Web UI/Chainlit auth commented out
|
||||||
|
7. **No Security Headers** - CSP, HSTS, X-Frame-Options missing
|
||||||
|
8. **Insecure Logging** - Sensitive data (API keys, positions) in logs
|
||||||
|
9. **No HTTPS/TLS Enforcement** - HTTP only
|
||||||
|
10. **Dependency Vulnerabilities** - No scanning (Dependabot, Snyk)
|
||||||
|
11. **Weak Secrets Management** - No vault, rotation, or encryption
|
||||||
|
12. **No Session Management** - For future multi-user scenarios
|
||||||
|
|
||||||
|
### P2 - Enterprise (8 issues)
|
||||||
|
13. **No Audit Logging** - Trade decisions untracked
|
||||||
|
14. **No Encryption at Rest** - Strategies, portfolio data unencrypted
|
||||||
|
15. **Docker Running as Root** - Privilege escalation risk
|
||||||
|
16. **No Resource Limits** - DoS via CPU/memory exhaustion
|
||||||
|
17. **Debug Mode Enabled** - Information disclosure
|
||||||
|
18. **No CORS Policy** - Cross-origin risks
|
||||||
|
19. **No Penetration Testing** - Framework needed
|
||||||
|
20. **No Compliance Documentation** - SOC 2, FINRA requirements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### Pattern Recognition
|
||||||
|
- **Symptom:** Multiple path-related vulnerabilities
|
||||||
|
- **Root Cause:** No centralized input validation
|
||||||
|
- **Solution:** Create `tradingagents/security/validators.py` module
|
||||||
|
|
||||||
|
### Development Practices
|
||||||
|
- ❌ Security features disabled for "convenience" (Jupyter tokens, Chainlit auth)
|
||||||
|
- ❌ Debug mode as default
|
||||||
|
- ❌ No security tests in 174-test suite
|
||||||
|
- ✅ Good: Strong engineering (89% coverage, type hints, logging)
|
||||||
|
|
||||||
|
### Security Debt Management
|
||||||
|
- Document everything in `docs/security/`
|
||||||
|
- Prioritize by risk (P0/P1/P2)
|
||||||
|
- Phased roadmap (3-6 months)
|
||||||
|
- Track with issue IDs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Metrics
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- 3 files changed
|
||||||
|
- 65 insertions, 3 deletions
|
||||||
|
- ~20 minutes implementation time
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- Validation logic tested with attack vectors
|
||||||
|
- All tests passing ✓
|
||||||
|
- Zero breaking changes
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- 740 lines of security docs created
|
||||||
|
- 3 files in `docs/security/`:
|
||||||
|
- `README.md` - Navigation
|
||||||
|
- `PR281_CRITICAL_FIXES.md` - Implementation guide
|
||||||
|
- `FUTURE_HARDENING.md` - 20-issue roadmap
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
### What Worked Well
|
||||||
|
✅ **Parallel sub-agent teams** - Security expert, file upload expert, architect
|
||||||
|
✅ **Organized docs** - No root clutter, clean structure
|
||||||
|
✅ **Testing mindset** - Verified with attack vectors
|
||||||
|
✅ **User-friendly** - CLI validation has helpful errors
|
||||||
|
|
||||||
|
### What to Remember
|
||||||
|
🎯 **Input validation is critical** - Trust no user input
|
||||||
|
🎯 **Defense in depth** - Multiple validation layers
|
||||||
|
🎯 **Fail secure, not open** - Default to restrictive
|
||||||
|
🎯 **Document technical debt** - Don't ignore, track it
|
||||||
|
|
||||||
|
### Reusable Patterns
|
||||||
|
|
||||||
|
**Validation Function Template:**
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
def validate_user_input(input_str: str, context: str) -> str:
|
||||||
|
"""Centralized validation pattern."""
|
||||||
|
# 1. Format check (regex)
|
||||||
|
# 2. Path traversal check (../, \\)
|
||||||
|
# 3. Length limits
|
||||||
|
# 4. Character whitelist
|
||||||
|
# 5. Normalize output (uppercase, trim)
|
||||||
|
# 6. Raise ValueError with clear message
|
||||||
|
return sanitized_input
|
||||||
|
```
|
||||||
|
|
||||||
|
**CLI Validation Loop Pattern:**
|
||||||
|
```python
|
||||||
|
def get_user_input():
|
||||||
|
"""User-friendly validation loop."""
|
||||||
|
while True:
|
||||||
|
value = prompt_user()
|
||||||
|
try:
|
||||||
|
validate(value)
|
||||||
|
return value
|
||||||
|
except ValueError as e:
|
||||||
|
console.print(f"[red]Error: {e}[/red]")
|
||||||
|
# Loop continues, user tries again
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tools & Resources Used
|
||||||
|
|
||||||
|
**Analysis:**
|
||||||
|
- Grep, Glob, Read tools for codebase exploration
|
||||||
|
- WebFetch for PR/review extraction
|
||||||
|
- Multi-agent analysis (parallel execution)
|
||||||
|
|
||||||
|
**Security References:**
|
||||||
|
- [CWE-22: Path Traversal](https://cwe.mitre.org/data/definitions/22.html)
|
||||||
|
- [CWE-284: Improper Access Control](https://cwe.mitre.org/data/definitions/284.html)
|
||||||
|
- [OWASP Top 10 2021](https://owasp.org/www-project-top-ten/)
|
||||||
|
- [OWASP LLM Top 10](https://owasp.org/www-project-top-10-for-large-language-model-applications/)
|
||||||
|
|
||||||
|
**Python Security:**
|
||||||
|
- `re` module for input validation
|
||||||
|
- Type hints for documentation
|
||||||
|
- Exception handling with clear messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Work
|
||||||
|
|
||||||
|
**Immediate (Month 1):**
|
||||||
|
- [ ] Fix remaining P0 issues (5 items)
|
||||||
|
- [ ] Add security test suite
|
||||||
|
- [ ] Enable pre-commit hooks (Bandit, secret scanning)
|
||||||
|
|
||||||
|
**Short-term (Month 3):**
|
||||||
|
- [ ] Implement authentication framework
|
||||||
|
- [ ] Add rate limiting
|
||||||
|
- [ ] Security headers & CORS
|
||||||
|
- [ ] Dependency scanning CI/CD
|
||||||
|
|
||||||
|
**Long-term (Month 6):**
|
||||||
|
- [ ] Vault integration for secrets
|
||||||
|
- [ ] Comprehensive audit logging
|
||||||
|
- [ ] Penetration testing
|
||||||
|
- [ ] Compliance documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
TradingAgents/
|
||||||
|
├── docs/
|
||||||
|
│ └── security/
|
||||||
|
│ ├── README.md # Navigation hub
|
||||||
|
│ ├── PR281_CRITICAL_FIXES.md # Implementation guide
|
||||||
|
│ └── FUTURE_HARDENING.md # 20-issue roadmap
|
||||||
|
├── tradingagents/
|
||||||
|
│ ├── agents/utils/memory.py # ✓ Fixed: ChromaDB reset
|
||||||
|
│ └── dataflows/local.py # ✓ Fixed: Path traversal validation
|
||||||
|
└── cli/
|
||||||
|
└── main.py # ✓ Fixed: CLI input validation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Commands
|
||||||
|
|
||||||
|
**Test validation locally:**
|
||||||
|
```bash
|
||||||
|
python -c "
|
||||||
|
from tradingagents.dataflows.local import validate_ticker_symbol
|
||||||
|
try:
|
||||||
|
validate_ticker_symbol('../../etc/passwd')
|
||||||
|
print('FAIL - attack not blocked')
|
||||||
|
except ValueError:
|
||||||
|
print('PASS - attack blocked')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check ChromaDB setting:**
|
||||||
|
```bash
|
||||||
|
grep -n "allow_reset" tradingagents/agents/utils/memory.py
|
||||||
|
# Should show: allow_reset=False
|
||||||
|
```
|
||||||
|
|
||||||
|
**View security docs:**
|
||||||
|
```bash
|
||||||
|
cd docs/security/
|
||||||
|
cat README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related PRs
|
||||||
|
|
||||||
|
- **PR #281** - Original multi-LLM/web UI PR (triggered review)
|
||||||
|
- **This PR** - Security fixes branch `claude/fix-gemini-review-issues-*`
|
||||||
|
- Commit 1: `docs: Add comprehensive security analysis`
|
||||||
|
- Commit 2: `security: Apply critical security fixes`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status:** ✅ Merged
|
||||||
|
**Risk Reduction:** Critical path traversal and data loss vulnerabilities eliminated
|
||||||
|
**Technical Debt:** 17 additional issues documented for future work
|
||||||
Loading…
Reference in New Issue