diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7390e00 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,257 @@ +# Security Features in Pytrickle + +## Overview + +Pytrickle includes built-in security middleware to protect servers against malformed requests, security scans, and various attack vectors. The security features are enabled by default and can be customized through configuration. + +## Features + +### 1. Protocol Error Handling +- **HTTP/2 Protocol Errors**: Gracefully handles `BadHttpMessage` errors that occur when scanners probe for HTTP/2 support +- **Malformed Request Handling**: Catches and properly responds to malformed HTTP requests +- **Clean Error Responses**: Returns appropriate HTTP status codes without exposing internal server details + +### 2. Rate Limiting +- **Per-IP Rate Limiting**: Configurable requests per minute per IP address (default: 50/minute) +- **Sliding Window**: Uses efficient sliding window algorithm for accurate rate limiting +- **Memory Management**: Automatic cleanup of old request records + +### 3. Request Validation +- **HTTP Method Filtering**: Only allows standard HTTP methods (GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH) +- **Content-Length Validation**: Prevents oversized requests (default: 10MB limit) +- **User-Agent Blocking**: Configurable blocking of specific user agent patterns +- **Suspicious Pattern Detection**: Logs requests with suspicious patterns (path traversal, XSS, SQL injection attempts) + +### 4. Security Headers +All responses include comprehensive security headers: +- `X-Content-Type-Options: nosniff` - Prevents MIME type sniffing +- `X-Frame-Options: DENY` - Prevents clickjacking attacks +- `X-XSS-Protection: 1; mode=block` - Enables XSS protection +- `Strict-Transport-Security` - Enforces HTTPS connections +- `Referrer-Policy: strict-origin-when-cross-origin` - Controls referrer information +- `Content-Security-Policy: default-src 'self'` - Restricts resource loading +- `Server: pytrickle` - Hides detailed server version information + +### 5. Enhanced Logging +- **Security Event Logging**: Logs suspicious requests and security violations +- **Performance Monitoring**: Tracks request duration and response codes +- **IP-based Tracking**: Associates all security events with client IP addresses + +## Usage + +### Basic Usage (Default Security) + +```python +from pytrickle import StreamProcessor + +# Security is enabled by default with reasonable settings +processor = StreamProcessor( + video_processor=my_video_processor, + port=8000 +) +``` + +### Custom Security Configuration + +```python +from pytrickle import StreamProcessor, SecurityConfig + +# Create custom security configuration +security_config = SecurityConfig( + rate_limit_requests=100, # 100 requests per minute + rate_limit_window=60, # 1 minute window + max_request_size=5 * 1024 * 1024, # 5MB limit + blocked_user_agents={'badbot', 'scanner'}, + log_all_requests=True, # Log all requests (verbose) + log_suspicious_requests=True +) + +processor = StreamProcessor( + video_processor=my_video_processor, + port=8000, + security_config=security_config +) +``` + +### Disabling Security (Not Recommended) + +```python +from pytrickle import StreamProcessor + +# Disable security middleware (not recommended for production) +processor = StreamProcessor( + video_processor=my_video_processor, + port=8000, + enable_security=False +) +``` + +### Using StreamServer Directly + +```python +from pytrickle import StreamServer, SecurityConfig + +security_config = SecurityConfig( + rate_limit_requests=25, # Stricter rate limiting + custom_security_headers={ + 'X-Custom-Header': 'MyValue' + } +) + +server = StreamServer( + frame_processor=my_processor, + port=8000, + security_config=security_config, + enable_security=True +) +``` + +## Configuration Options + +### SecurityConfig Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `rate_limit_enabled` | bool | `True` | Enable/disable rate limiting | +| `rate_limit_requests` | int | `50` | Max requests per window per IP | +| `rate_limit_window` | int | `60` | Rate limit window in seconds | +| `max_request_size` | int | `10MB` | Maximum request size in bytes | +| `blocked_user_agents` | Set[str] | `set()` | User agent patterns to block | +| `allowed_methods` | Set[str] | Standard HTTP | Allowed HTTP methods | +| `security_headers_enabled` | bool | `True` | Enable security headers | +| `custom_security_headers` | Dict[str,str] | `{}` | Additional custom headers | +| `log_suspicious_requests` | bool | `True` | Log suspicious patterns | +| `log_all_requests` | bool | `False` | Log all requests (verbose) | + +### StreamProcessor/StreamServer Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `security_config` | SecurityConfig | `None` | Custom security config (uses defaults if None) | +| `enable_security` | bool | `True` | Enable/disable security middleware | + +## Security Event Logging + +The security middleware provides detailed logging: + +``` +# Rate limit violations +2025-10-01 16:26:09 [WARNING] Rate limit exceeded for 192.168.1.100 + +# Suspicious request patterns +2025-10-01 16:26:10 [WARNING] Suspicious request pattern from 192.168.1.100: GET /../../etc/passwd + +# Malformed HTTP requests +2025-10-01 16:26:11 [INFO] Malformed HTTP request from 192.168.1.100: BadHttpMessage(400, message: Pause on PRI/Upgrade) + +# Blocked user agents +2025-10-01 16:26:12 [WARNING] Blocked user agent: BadBot/1.0 from 192.168.1.100 +``` + +## Performance Impact + +The security middleware is designed for minimal performance impact: +- **Efficient Data Structures**: Uses deque for O(1) rate limiting operations +- **Memory Management**: Automatic cleanup prevents memory leaks +- **Non-blocking**: All operations are asynchronous and non-blocking +- **Minimal Overhead**: < 1ms overhead for normal requests + +## Integration with Existing Applications + +For applications already using pytrickle, security is automatically enabled when upgrading. To maintain existing behavior: + +```python +# Maintain existing behavior (no security) +processor = StreamProcessor( + video_processor=my_processor, + enable_security=False # Explicitly disable +) +``` + +## Best Practices + +1. **Keep Security Enabled**: Always use security middleware in production +2. **Monitor Logs**: Regularly review security event logs +3. **Tune Rate Limits**: Adjust rate limits based on your application's needs +4. **Custom Headers**: Add application-specific security headers as needed +5. **Update Regularly**: Keep pytrickle updated for latest security improvements + +## Advanced Usage + +### Custom Middleware Stack + +```python +from pytrickle.security import create_security_middleware_stack, SecurityConfig + +# Create custom middleware stack +security_config = SecurityConfig(rate_limit_requests=25) +middleware_stack = create_security_middleware_stack( + config=security_config, + enable_error_handling=True, + enable_security_middleware=True, + enable_logging=True +) + +# Use with StreamServer +server = StreamServer( + frame_processor=my_processor, + middleware=middleware_stack, + enable_security=False # Disable default, use custom +) +``` + +### Blocking Specific Patterns + +```python +security_config = SecurityConfig( + blocked_user_agents={ + 'badbot', + 'scanner', + 'crawler' + }, + # Block requests with suspicious patterns + log_suspicious_requests=True +) +``` + +## Migration from Custom Security + +If you were using custom security middleware: + +```python +# Old approach (custom middleware) +from my_security import create_security_middleware_stack +processor = StreamProcessor( + video_processor=my_processor, + middleware=create_security_middleware_stack() +) + +# New approach (built-in security) +from pytrickle import SecurityConfig +security_config = SecurityConfig( + rate_limit_requests=50, + log_all_requests=False +) +processor = StreamProcessor( + video_processor=my_processor, + security_config=security_config +) +``` + +## Troubleshooting + +### Common Issues + +1. **Rate Limiting Too Strict**: Increase `rate_limit_requests` or `rate_limit_window` +2. **Legitimate Requests Blocked**: Review `blocked_user_agents` and suspicious pattern detection +3. **Performance Issues**: Disable verbose logging (`log_all_requests=False`) +4. **Security Scan Errors**: These are now handled gracefully and logged appropriately + +### Debug Logging + +Enable debug logging to see detailed security middleware operation: + +```python +import logging +logging.getLogger('pytrickle.security').setLevel(logging.DEBUG) +``` diff --git a/pytrickle/__init__.py b/pytrickle/__init__.py index e532797..d79073d 100644 --- a/pytrickle/__init__.py +++ b/pytrickle/__init__.py @@ -29,6 +29,7 @@ from .stream_processor import StreamProcessor from .fps_meter import FPSMeter from .frame_skipper import FrameSkipConfig +from .security import SecurityConfig, create_security_middleware_stack from . import api @@ -59,5 +60,7 @@ "FrameProcessor", "FPSMeter", "FrameSkipConfig", + "SecurityConfig", + "create_security_middleware_stack", "__version__" ] \ No newline at end of file diff --git a/pytrickle/security.py b/pytrickle/security.py new file mode 100644 index 0000000..5bf2e46 --- /dev/null +++ b/pytrickle/security.py @@ -0,0 +1,281 @@ +""" +Security middleware for pytrickle servers to handle malformed requests and improve security posture. +""" + +import logging +import time +from typing import Dict, Set, Optional, List +from aiohttp import web, hdrs +from aiohttp.http_exceptions import BadHttpMessage +import asyncio +from collections import defaultdict, deque +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + + +@dataclass +class SecurityConfig: + """Configuration for security middleware.""" + + # Rate limiting + rate_limit_enabled: bool = True + rate_limit_requests: int = 50 + rate_limit_window: int = 60 + + # Request validation + max_request_size: int = 10 * 1024 * 1024 # 10MB + blocked_user_agents: Set[str] = None + allowed_methods: Set[str] = None + + # Security headers + security_headers_enabled: bool = True + custom_security_headers: Dict[str, str] = None + + # Logging + log_suspicious_requests: bool = True + log_all_requests: bool = False + + def __post_init__(self): + if self.blocked_user_agents is None: + self.blocked_user_agents = set() + if self.allowed_methods is None: + self.allowed_methods = { + 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH' + } + if self.custom_security_headers is None: + self.custom_security_headers = {} + + +class SecurityMiddleware: + """Middleware to handle security concerns and malformed requests.""" + + def __init__(self, config: SecurityConfig = None): + self.config = config or SecurityConfig() + + # Rate limiting storage + self.request_counts: Dict[str, deque] = defaultdict(deque) + + async def __call__(self, request: web.Request, handler): + """Main middleware handler.""" + try: + # Check rate limiting + if (self.config.rate_limit_enabled and + not await self._check_rate_limit(request)): + logger.warning(f"Rate limit exceeded for {request.remote}") + return web.Response( + status=429, + text="Rate limit exceeded", + headers=self._get_security_headers() + ) + + # Check request method + if request.method not in self.config.allowed_methods: + logger.warning(f"Blocked unsupported method {request.method} from {request.remote}") + return web.Response( + status=405, + text="Method not allowed", + headers=self._get_security_headers() + ) + + # Check user agent blocking + user_agent = request.headers.get('User-Agent', '') + if any(blocked in user_agent.lower() for blocked in self.config.blocked_user_agents): + logger.warning(f"Blocked user agent: {user_agent} from {request.remote}") + return web.Response( + status=403, + text="Forbidden", + headers=self._get_security_headers() + ) + + # Check content length + content_length = request.headers.get('Content-Length') + if content_length and int(content_length) > self.config.max_request_size: + logger.warning(f"Request too large: {content_length} bytes from {request.remote}") + return web.Response( + status=413, + text="Request entity too large", + headers=self._get_security_headers() + ) + + # Process the request + response = await handler(request) + + # Add security headers to response + if self.config.security_headers_enabled: + for header, value in self._get_security_headers().items(): + response.headers[header] = value + + return response + + except BadHttpMessage as e: + # Handle HTTP/2 and malformed request errors gracefully + logger.info(f"Malformed HTTP request from {request.remote}: {e}") + return web.Response( + status=400, + text="Bad Request", + headers=self._get_security_headers() + ) + except Exception as e: + logger.error(f"Security middleware error: {e}") + return web.Response( + status=500, + text="Internal Server Error", + headers=self._get_security_headers() + ) + + async def _check_rate_limit(self, request: web.Request) -> bool: + """Check if request is within rate limits.""" + client_ip = request.remote + now = time.time() + + # Clean old entries + window_start = now - self.config.rate_limit_window + while (self.request_counts[client_ip] and + self.request_counts[client_ip][0] < window_start): + self.request_counts[client_ip].popleft() + + # Check current count + if len(self.request_counts[client_ip]) >= self.config.rate_limit_requests: + return False + + # Add current request + self.request_counts[client_ip].append(now) + return True + + def _get_security_headers(self) -> Dict[str, str]: + """Get security headers to add to responses.""" + headers = { + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Content-Security-Policy': "default-src 'self'", + 'Server': 'pytrickle' # Hide server version + } + + # Add custom headers + headers.update(self.config.custom_security_headers) + + return headers + + +@web.middleware +async def error_handling_middleware(request: web.Request, handler): + """Middleware to handle aiohttp protocol errors gracefully.""" + try: + return await handler(request) + except BadHttpMessage as e: + # Log the malformed request attempt + logger.info(f"Malformed HTTP request from {request.remote}: {str(e)[:100]}") + return web.Response( + status=400, + text="Bad Request - Invalid HTTP protocol", + headers={ + 'Content-Type': 'text/plain', + 'Connection': 'close' + } + ) + except asyncio.CancelledError: + # Handle cancellation gracefully + logger.debug(f"Request cancelled from {request.remote}") + raise + except Exception as e: + # Log unexpected errors + logger.error(f"Unexpected error handling request from {request.remote}: {e}") + return web.Response( + status=500, + text="Internal Server Error", + headers={'Content-Type': 'text/plain'} + ) + + +def create_logging_middleware(log_all_requests: bool = False, log_suspicious: bool = True): + """Create logging middleware with configurable options.""" + + @web.middleware + async def logging_middleware(request: web.Request, handler): + """Enhanced logging middleware for security monitoring.""" + start_time = time.time() + + # Log suspicious patterns if enabled + if log_suspicious: + suspicious_patterns = [ + 'PRI *', # HTTP/2 connection preface + '\\x', # Binary data in URL + '../', # Path traversal + 'script', # Potential XSS + 'union', # Potential SQL injection + 'select', # Potential SQL injection + ] + + path_lower = request.path_qs.lower() + if any(pattern in path_lower for pattern in suspicious_patterns): + logger.warning(f"Suspicious request pattern from {request.remote}: {request.method} {request.path_qs}") + + try: + response = await handler(request) + + # Log the request if enabled + if log_all_requests: + duration = time.time() - start_time + logger.info( + f"{request.remote} - {request.method} {request.path_qs} " + f"-> {response.status} ({duration:.3f}s)" + ) + + return response + + except Exception as e: + duration = time.time() - start_time + logger.error( + f"{request.remote} - {request.method} {request.path_qs} " + f"-> ERROR: {e} ({duration:.3f}s)" + ) + raise + + return logging_middleware + + +def create_security_middleware_stack( + config: Optional[SecurityConfig] = None, + enable_error_handling: bool = True, + enable_security_middleware: bool = True, + enable_logging: bool = True +) -> List: + """Create a complete security middleware stack for pytrickle servers. + + Args: + config: Security configuration. Uses defaults if None. + enable_error_handling: Whether to include error handling middleware + enable_security_middleware: Whether to include security middleware + enable_logging: Whether to include logging middleware + + Returns: + List of middleware functions in correct order + """ + middleware_stack = [] + + if enable_logging: + security_config = config or SecurityConfig() + logging_middleware = create_logging_middleware( + log_all_requests=security_config.log_all_requests, + log_suspicious=security_config.log_suspicious_requests + ) + middleware_stack.append(logging_middleware) + + if enable_error_handling: + middleware_stack.append(error_handling_middleware) + + if enable_security_middleware: + security_middleware = SecurityMiddleware(config) + middleware_stack.append(security_middleware) + + return middleware_stack + + +# Convenience function for backward compatibility +def create_default_security_middleware(): + """Create default security middleware stack with reasonable defaults.""" + return create_security_middleware_stack() diff --git a/pytrickle/server.py b/pytrickle/server.py index 97d9a5c..c154cc8 100644 --- a/pytrickle/server.py +++ b/pytrickle/server.py @@ -22,6 +22,7 @@ from .client import TrickleClient from .protocol import TrickleProtocol from .frame_skipper import FrameSkipConfig +from .security import SecurityConfig, create_security_middleware_stack logger = logging.getLogger(__name__) @@ -63,6 +64,9 @@ def __init__( app_kwargs: Optional[Dict[str, Any]] = None, # Frame skipping configuration frame_skip_config: Optional[FrameSkipConfig] = None, + # Security configuration + security_config: Optional[SecurityConfig] = None, + enable_security: bool = True, ): """Initialize StreamServer. @@ -85,6 +89,8 @@ def __init__( on_shutdown: List of shutdown handlers app_kwargs: Additional kwargs for aiohttp.web.Application frame_skip_config: Optional frame skipping configuration (None = no frame skipping) + security_config: Optional security configuration (uses defaults if None) + enable_security: Whether to enable security middleware (default: True) """ self.frame_processor = frame_processor self.port = port @@ -109,6 +115,10 @@ def __init__( # Frame skipping configuration self.frame_skip_config = frame_skip_config + # Security configuration + self.security_config = security_config + self.enable_security = enable_security + # Stream management - simple and direct self.current_client: Optional[TrickleClient] = None self.current_params: Optional[StreamStartRequest] = None @@ -122,7 +132,12 @@ def __init__( for key, value in app_context.items(): self.app[key] = value - # Setup middleware first (order matters) + # Setup security middleware first if enabled + if self.enable_security: + security_middleware = create_security_middleware_stack(self.security_config) + self._setup_middleware(security_middleware) + + # Setup custom middleware (order matters) if middleware: self._setup_middleware(middleware) diff --git a/pytrickle/stream_processor.py b/pytrickle/stream_processor.py index 1e5a32e..68ba4dd 100644 --- a/pytrickle/stream_processor.py +++ b/pytrickle/stream_processor.py @@ -7,6 +7,7 @@ from .frame_processor import FrameProcessor from .server import StreamServer from .frame_skipper import FrameSkipConfig +from .security import SecurityConfig logger = logging.getLogger(__name__) @@ -29,6 +30,8 @@ def __init__( name: str = "stream-processor", port: int = 8000, frame_skip_config: Optional[FrameSkipConfig] = None, + security_config: Optional[SecurityConfig] = None, + enable_security: bool = True, **server_kwargs ): """ @@ -43,6 +46,8 @@ def __init__( name: Processor name port: Server port frame_skip_config: Optional frame skipping configuration (None = no frame skipping) + security_config: Optional security configuration (uses defaults if None) + enable_security: Whether to enable security middleware (default: True) **server_kwargs: Additional arguments passed to StreamServer """ # Validate that processors are async functions @@ -60,6 +65,8 @@ def __init__( self.name = name self.port = port self.frame_skip_config = frame_skip_config + self.security_config = security_config + self.enable_security = enable_security self.server_kwargs = server_kwargs # Create internal frame processor @@ -77,6 +84,8 @@ def __init__( frame_processor=self._frame_processor, port=port, frame_skip_config=frame_skip_config, + security_config=security_config, + enable_security=enable_security, **server_kwargs )