320 lines
9.0 KiB
Markdown
320 lines
9.0 KiB
Markdown
# EU-Utility Security Fixes Applied
|
|
|
|
**Date:** 2026-02-14
|
|
**Auditor:** Security Auditor Agent
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
This document details the security fixes applied during the security audit of EU-Utility.
|
|
|
|
**Modules Fixed:** 4
|
|
**Security Improvements:** 15+
|
|
**Risk Level Reduced:** MEDIUM-HIGH → LOW-MEDIUM
|
|
|
|
---
|
|
|
|
## Fixes Applied
|
|
|
|
### 1. ✅ data_store.py - Path Traversal Protection
|
|
|
|
**Action:** Replaced vulnerable module with secure version
|
|
|
|
**Changes:**
|
|
- Backup created: `data_store_vulnerable.py`
|
|
- Active module now: `data_store_secure.py`
|
|
|
|
**Security Features Added:**
|
|
- Path validation using `PathValidator` class
|
|
- Resolved path verification against base directory
|
|
- Plugin ID validation (type checking, empty checks)
|
|
- Key validation against dangerous patterns
|
|
- Data structure validation before save
|
|
- Backup path traversal protection
|
|
|
|
**Code Example:**
|
|
```python
|
|
def _get_plugin_file(self, plugin_id: str) -> Path:
|
|
# Validate plugin_id
|
|
if not isinstance(plugin_id, str):
|
|
raise SecurityError("plugin_id must be a string")
|
|
|
|
# Sanitize and validate
|
|
safe_name = PathValidator.sanitize_filename(plugin_id, '_')
|
|
if '..' in safe_name or '/' in safe_name or '\\' in safe_name:
|
|
raise SecurityError(f"Invalid characters in plugin_id: {plugin_id}")
|
|
|
|
# Verify resolved path is within base directory
|
|
file_path = self.data_dir / f"{safe_name}.json"
|
|
resolved_path = file_path.resolve()
|
|
if not str(resolved_path).startswith(str(self._base_path)):
|
|
raise SecurityError(f"Path traversal detected: {plugin_id}")
|
|
|
|
return file_path
|
|
```
|
|
|
|
---
|
|
|
|
### 2. ✅ screenshot.py - Filename Sanitization & Path Validation
|
|
|
|
**Action:** Replaced vulnerable module with secure version
|
|
|
|
**Changes:**
|
|
- Backup created: `screenshot_vulnerable.py`
|
|
- Active module now: `screenshot_secure.py`
|
|
|
|
**Security Features Added:**
|
|
- Filename sanitization using `PathValidator.sanitize_filename()`
|
|
- Path resolution validation against base save path
|
|
- Region coordinate validation (prevent DoS via huge regions)
|
|
- Window handle validation (type and value checking)
|
|
- Dimension sanity checks
|
|
|
|
**Code Example:**
|
|
```python
|
|
def save_screenshot(self, image: Image.Image, filename: Optional[str] = None) -> Path:
|
|
# Sanitize filename
|
|
safe_filename = PathValidator.sanitize_filename(filename, '_')
|
|
|
|
filepath = self._save_path / safe_filename
|
|
|
|
# Security check: ensure resolved path is within save_path
|
|
try:
|
|
resolved_path = filepath.resolve()
|
|
if not str(resolved_path).startswith(str(self._base_save_path)):
|
|
raise SecurityError("Path traversal detected in filename")
|
|
except (OSError, ValueError) as e:
|
|
# Fallback to safe default
|
|
safe_filename = f"screenshot_{int(time.time())}.{self._format.lower()}"
|
|
filepath = self._save_path / safe_filename
|
|
```
|
|
|
|
---
|
|
|
|
### 3. ✅ clipboard.py - Input Validation & Size Limits
|
|
|
|
**Action:** Enhanced with security validation
|
|
|
|
**Security Features Added:**
|
|
- Maximum text length limit (10KB per entry)
|
|
- Maximum total storage limit (1MB)
|
|
- Null byte detection and rejection
|
|
- Text sanitization (control character removal)
|
|
- Source string length limiting
|
|
- Secure file permissions (0o600 - owner only)
|
|
- Temporary file atomic write pattern
|
|
|
|
**Code Example:**
|
|
```python
|
|
def _validate_text(self, text: str) -> tuple[bool, str]:
|
|
if not isinstance(text, str):
|
|
return False, "Text must be a string"
|
|
|
|
if '\x00' in text:
|
|
return False, "Text contains null bytes"
|
|
|
|
if len(text) > self._max_text_length:
|
|
return False, f"Text exceeds maximum length"
|
|
|
|
# Auto-cleanup to make room
|
|
current_size = sum(len(entry.text) for entry in self._history)
|
|
if current_size + len(text) > self._max_total_storage:
|
|
while self._history and current_size + len(text) > self._max_total_storage:
|
|
removed = self._history.popleft()
|
|
current_size -= len(removed.text)
|
|
|
|
return True, ""
|
|
|
|
def _save_history(self):
|
|
# Write with restricted permissions
|
|
temp_path = self._history_file.with_suffix('.tmp')
|
|
with open(temp_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
os.chmod(temp_path, 0o600) # Owner read/write only
|
|
temp_path.replace(self._history_file)
|
|
```
|
|
|
|
---
|
|
|
|
### 4. ✅ http_client.py - URL Validation & SSRF Protection
|
|
|
|
**Action:** Enhanced with security validation
|
|
|
|
**Security Features Added:**
|
|
- URL scheme validation (only http:// and https:// allowed)
|
|
- Path traversal pattern detection (`..`, `@`, `\`, null bytes)
|
|
- SSRF protection (blocks private, loopback, reserved, link-local IPs)
|
|
- Custom exception classes for security errors
|
|
- URL validation on both GET and POST requests
|
|
|
|
**Code Example:**
|
|
```python
|
|
def _validate_url(self, url: str) -> str:
|
|
if not url:
|
|
raise URLSecurityError("URL cannot be empty")
|
|
|
|
from urllib.parse import urlparse
|
|
parsed = urlparse(url)
|
|
|
|
# Check scheme
|
|
allowed_schemes = {'http', 'https'}
|
|
if parsed.scheme not in allowed_schemes:
|
|
raise URLSecurityError(f"URL scheme '{parsed.scheme}' not allowed")
|
|
|
|
# Check for dangerous patterns
|
|
dangerous_patterns = ['..', '@', '\\', '\x00']
|
|
for pattern in dangerous_patterns:
|
|
if pattern in url:
|
|
raise URLSecurityError(f"URL contains dangerous pattern")
|
|
|
|
# SSRF protection - block private IPs
|
|
hostname = parsed.hostname
|
|
if hostname:
|
|
try:
|
|
ip = ipaddress.ip_address(hostname)
|
|
if ip.is_private or ip.is_loopback or ip.is_reserved:
|
|
raise URLSecurityError(f"URL resolves to restricted IP")
|
|
except ValueError:
|
|
pass # Not an IP, it's a hostname
|
|
|
|
return url
|
|
```
|
|
|
|
---
|
|
|
|
## Files Modified
|
|
|
|
| File | Action | Status |
|
|
|------|--------|--------|
|
|
| `core/data_store.py` | Replaced with secure version | ✅ Fixed |
|
|
| `core/data_store_vulnerable.py` | Backup created | ✅ Archived |
|
|
| `core/screenshot.py` | Replaced with secure version | ✅ Fixed |
|
|
| `core/screenshot_vulnerable.py` | Backup created | ✅ Archived |
|
|
| `core/clipboard.py` | Enhanced with validation | ✅ Fixed |
|
|
| `core/http_client.py` | Added URL validation | ✅ Fixed |
|
|
|
|
---
|
|
|
|
## Remaining Recommendations
|
|
|
|
### Medium Priority (Not Yet Implemented)
|
|
|
|
1. **Plugin Manager Security**
|
|
- Implement plugin signature verification
|
|
- Add permission manifest system
|
|
- Consider sandboxed execution environment
|
|
|
|
2. **Audit Logging**
|
|
- Log all security violations
|
|
- Log plugin loading/unloading
|
|
- Log file operations outside data directories
|
|
|
|
### Low Priority (Future Enhancements)
|
|
|
|
3. **Data Encryption**
|
|
- Encrypt sensitive plugin data at rest
|
|
- Encrypt clipboard history with user password
|
|
|
|
4. **Rate Limiting**
|
|
- Per-plugin API rate limits
|
|
- Screenshot capture rate limiting
|
|
|
|
---
|
|
|
|
## Testing Security Fixes
|
|
|
|
### Path Traversal Test
|
|
```python
|
|
from core.data_store import get_data_store
|
|
|
|
data_store = get_data_store()
|
|
|
|
# Should raise SecurityError
|
|
try:
|
|
data_store.save("../../../etc/passwd", "key", "data")
|
|
except SecurityError:
|
|
print("✅ Path traversal blocked in data_store")
|
|
|
|
# Should raise SecurityError
|
|
try:
|
|
from core.screenshot import get_screenshot_service
|
|
service = get_screenshot_service()
|
|
service.save_screenshot(image, "../../../malware.exe")
|
|
except SecurityError:
|
|
print("✅ Path traversal blocked in screenshot")
|
|
```
|
|
|
|
### URL Validation Test
|
|
```python
|
|
from core.http_client import HTTPClient, URLSecurityError
|
|
|
|
client = HTTPClient()
|
|
|
|
# Should raise URLSecurityError
|
|
try:
|
|
client.get("file:///etc/passwd")
|
|
except URLSecurityError:
|
|
print("✅ File protocol blocked")
|
|
|
|
try:
|
|
client.get("http://127.0.0.1/admin")
|
|
except URLSecurityError:
|
|
print("✅ Localhost blocked (SSRF protection)")
|
|
|
|
try:
|
|
client.get("http://192.168.1.1/admin")
|
|
except URLSecurityError:
|
|
print("✅ Private IP blocked (SSRF protection)")
|
|
```
|
|
|
|
### Clipboard Validation Test
|
|
```python
|
|
from core.clipboard import get_clipboard_manager
|
|
|
|
clipboard = get_clipboard_manager()
|
|
|
|
# Should fail - too large
|
|
result = clipboard.copy("x" * (11 * 1024)) # 11KB
|
|
assert result == False, "Should reject oversized text"
|
|
print("✅ Size limit enforced")
|
|
|
|
# Should sanitize null bytes
|
|
result = clipboard.copy("hello\x00world")
|
|
assert result == False, "Should reject null bytes"
|
|
print("✅ Null byte protection working")
|
|
```
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
- [x] Path traversal protection (data_store)
|
|
- [x] Path traversal protection (screenshot)
|
|
- [x] Filename sanitization
|
|
- [x] Input validation (clipboard)
|
|
- [x] Size limits (clipboard)
|
|
- [x] URL validation (http_client)
|
|
- [x] SSRF protection (http_client)
|
|
- [x] Secure file permissions
|
|
- [x] Atomic file writes
|
|
- [x] Data structure validation
|
|
- [ ] Plugin signature verification (future)
|
|
- [ ] Plugin sandboxing (future)
|
|
- [ ] Audit logging (future)
|
|
- [ ] Data encryption (future)
|
|
|
|
---
|
|
|
|
## Contact
|
|
|
|
For questions about these security fixes, refer to:
|
|
- `SECURITY_AUDIT_REPORT.md` - Full audit report
|
|
- `core/security_utils.py` - Security utility classes
|
|
|
|
---
|
|
|
|
*Security fixes applied by Security Auditor Agent*
|
|
*EU-Utility Security Hardening 2026*
|