diff --git a/backend/core/security_middleware.py b/backend/core/security_middleware.py new file mode 100644 index 0000000..09626fc --- /dev/null +++ b/backend/core/security_middleware.py @@ -0,0 +1,96 @@ +""" +Security Headers Middleware for FastAPI + +This middleware adds security headers to all HTTP responses to protect against +common web vulnerabilities including XSS, clickjacking, and MIME sniffing attacks. + +Security Headers Implemented: +- Strict-Transport-Security (HSTS): Forces HTTPS connections +- Content-Security-Policy (CSP): Prevents XSS by restricting resource loading +- X-Frame-Options: Prevents clickjacking attacks +- X-Content-Type-Options: Prevents MIME sniffing attacks +- X-XSS-Protection: Legacy XSS protection for older browsers +""" + +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.types import ASGIApp + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + """ + Middleware that adds security headers to all HTTP responses. + + This middleware should be added to the FastAPI application to ensure + all responses include appropriate security headers that protect against + common web vulnerabilities. + + Example: + app = FastAPI() + app.add_middleware(SecurityHeadersMiddleware) + """ + + def __init__(self, app: ASGIApp): + """ + Initialize the security headers middleware. + + Args: + app: The ASGI application + """ + super().__init__(app) + + async def dispatch(self, request: Request, call_next): + """ + Process the request and add security headers to the response. + + Args: + request: The incoming HTTP request + call_next: The next middleware or route handler + + Returns: + Response with security headers added + """ + response = await call_next(request) + + # HSTS - HTTP Strict Transport Security + # Forces browsers to use HTTPS for all future connections to this domain + # max-age=31536000: Valid for 1 year (in seconds) + # includeSubDomains: Apply to all subdomains as well + response.headers["Strict-Transport-Security"] = ( + "max-age=31536000; includeSubDomains" + ) + + # CSP - Content Security Policy + # Restricts which resources can be loaded to prevent XSS attacks + # default-src 'self': Only allow resources from same origin by default + # script-src 'self' 'unsafe-inline': Allow scripts from same origin and inline scripts (needed for some frameworks) + # style-src 'self' 'unsafe-inline': Allow styles from same origin and inline styles + # img-src 'self' data: https:: Allow images from same origin, data URIs, and HTTPS + # font-src 'self' data:: Allow fonts from same origin and data URIs + # connect-src 'self' ws: wss:: Allow connections to same origin and WebSocket connections + response.headers["Content-Security-Policy"] = ( + "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self' data:; " + "connect-src 'self' ws: wss:;" + ) + + # X-Frame-Options - Clickjacking Protection + # DENY: Prevent the page from being displayed in a frame/iframe + # This protects against clickjacking attacks where malicious sites + # embed your app in an invisible iframe to trick users + response.headers["X-Frame-Options"] = "DENY" + + # X-Content-Type-Options - MIME Sniffing Protection + # nosniff: Prevents browsers from MIME-sniffing responses away from the declared content-type + # This prevents browsers from interpreting files as a different MIME type than declared + response.headers["X-Content-Type-Options"] = "nosniff" + + # X-XSS-Protection - Legacy XSS Protection + # 1; mode=block: Enable XSS filtering and block the page if attack is detected + # Note: This is a legacy header for older browsers. Modern browsers use CSP instead. + response.headers["X-XSS-Protection"] = "1; mode=block" + + return response diff --git a/backend/main.py b/backend/main.py index a2ed543..02867b9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,6 +8,7 @@ from backend.api.v1 import api_router from backend.websocket.manager import manager from backend.core.security import decode_token +from backend.core.security_middleware import SecurityHeadersMiddleware import logging logger = logging.getLogger(__name__) @@ -27,6 +28,9 @@ allow_headers=["*"], ) +# Security headers middleware +app.add_middleware(SecurityHeadersMiddleware) + # Include API v1 router app.include_router(api_router, prefix=settings.API_V1_PREFIX) diff --git a/backend/tests/core/test_security_middleware.py b/backend/tests/core/test_security_middleware.py new file mode 100644 index 0000000..403bd4d --- /dev/null +++ b/backend/tests/core/test_security_middleware.py @@ -0,0 +1,198 @@ +""" +Tests for Security Headers Middleware + +Validates that all security headers are correctly added to HTTP responses +and that the middleware doesn't break existing application functionality. +""" + +from fastapi.testclient import TestClient + +from backend.models.organization import Organization + + +class TestSecurityHeadersMiddleware: + """Test suite for security headers middleware""" + + def test_health_endpoint_has_all_security_headers(self, client: TestClient): + """Test that health check endpoint includes all security headers""" + # Act + response = client.get("/health") + + # Assert + assert response.status_code == 200 + + # Check all security headers are present + assert "Strict-Transport-Security" in response.headers + assert "Content-Security-Policy" in response.headers + assert "X-Frame-Options" in response.headers + assert "X-Content-Type-Options" in response.headers + assert "X-XSS-Protection" in response.headers + + def test_hsts_header_has_correct_value(self, client: TestClient): + """Test HSTS header enforces HTTPS with 1 year max-age""" + # Act + response = client.get("/health") + + # Assert + hsts_header = response.headers.get("Strict-Transport-Security") + assert hsts_header == "max-age=31536000; includeSubDomains" + + # Verify it includes required directives + assert "max-age=31536000" in hsts_header # 1 year in seconds + assert "includeSubDomains" in hsts_header + + def test_csp_header_has_correct_value(self, client: TestClient): + """Test CSP header restricts resource loading appropriately""" + # Act + response = client.get("/health") + + # Assert + csp_header = response.headers.get("Content-Security-Policy") + + # Verify all required CSP directives are present + assert "default-src 'self'" in csp_header + assert "script-src 'self' 'unsafe-inline'" in csp_header + assert "style-src 'self' 'unsafe-inline'" in csp_header + assert "img-src 'self' data: https:" in csp_header + assert "font-src 'self' data:" in csp_header + assert "connect-src 'self' ws: wss:" in csp_header + + def test_x_frame_options_prevents_clickjacking(self, client: TestClient): + """Test X-Frame-Options header prevents framing""" + # Act + response = client.get("/health") + + # Assert + assert response.headers.get("X-Frame-Options") == "DENY" + + def test_x_content_type_options_prevents_mime_sniffing(self, client: TestClient): + """Test X-Content-Type-Options prevents MIME sniffing""" + # Act + response = client.get("/health") + + # Assert + assert response.headers.get("X-Content-Type-Options") == "nosniff" + + def test_x_xss_protection_header_is_present(self, client: TestClient): + """Test X-XSS-Protection header is present for legacy browsers""" + # Act + response = client.get("/health") + + # Assert + assert response.headers.get("X-XSS-Protection") == "1; mode=block" + + def test_root_endpoint_has_security_headers(self, client: TestClient): + """Test that root endpoint also has security headers""" + # Act + response = client.get("/") + + # Assert + assert response.status_code == 200 + assert "Strict-Transport-Security" in response.headers + assert "Content-Security-Policy" in response.headers + assert "X-Frame-Options" in response.headers + + def test_api_endpoints_have_security_headers( + self, client: TestClient, sample_organization: Organization + ): + """Test that API endpoints have security headers""" + # Arrange + payload = { + "email": "sectest@test.com", + "name": "Security Test User", + "password": "SecurePass123!", + "organization_slug": sample_organization.slug, + } + + # Act + response = client.post("/api/v1/auth/register", json=payload) + + # Assert + assert response.status_code == 201 + assert "Strict-Transport-Security" in response.headers + assert "Content-Security-Policy" in response.headers + assert "X-Frame-Options" in response.headers + assert "X-Content-Type-Options" in response.headers + assert "X-XSS-Protection" in response.headers + + def test_error_responses_have_security_headers(self, client: TestClient): + """Test that error responses also include security headers""" + # Act - Request non-existent endpoint + response = client.get("/api/v1/nonexistent") + + # Assert + assert response.status_code == 404 + assert "Strict-Transport-Security" in response.headers + assert "Content-Security-Policy" in response.headers + assert "X-Frame-Options" in response.headers + + def test_authenticated_endpoints_have_security_headers( + self, client: TestClient, auth_headers: dict + ): + """Test that authenticated endpoints include security headers""" + # Act - Try to access authenticated endpoint (will fail but should have headers) + response = client.get("/api/v1/users/me", headers=auth_headers) + + # Assert - Response should have security headers regardless of success + assert "Strict-Transport-Security" in response.headers + assert "Content-Security-Policy" in response.headers + + def test_security_headers_do_not_break_json_responses( + self, client: TestClient, sample_organization: Organization + ): + """Test that security headers don't interfere with JSON response parsing""" + # Arrange + payload = { + "email": "jsontest@test.com", + "name": "JSON Test User", + "password": "JsonPass123!", + "organization_slug": sample_organization.slug, + } + + # Act + response = client.post("/api/v1/auth/register", json=payload) + + # Assert - Response should be valid JSON with security headers + assert response.status_code == 201 + data = response.json() # Should parse without error + assert data["email"] == "jsontest@test.com" + assert "Strict-Transport-Security" in response.headers + + def test_all_security_headers_present_together(self, client: TestClient): + """Test that all 5 security headers are present in every response""" + # Act + response = client.get("/health") + + # Assert - Count security headers + security_headers = [ + "Strict-Transport-Security", + "Content-Security-Policy", + "X-Frame-Options", + "X-Content-Type-Options", + "X-XSS-Protection", + ] + + for header in security_headers: + assert header in response.headers, f"Missing security header: {header}" + + def test_middleware_processes_requests_correctly(self, client: TestClient): + """Test that middleware doesn't break request processing""" + # Act + response = client.get("/health") + + # Assert + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + # Verify security headers are added (count the 5 specific headers) + security_headers = [ + "Strict-Transport-Security", + "Content-Security-Policy", + "X-Frame-Options", + "X-Content-Type-Options", + "X-XSS-Protection", + ] + present_headers = [h for h in security_headers if h in response.headers] + assert ( + len(present_headers) == 5 + ), f"Expected 5 security headers, found {len(present_headers)}" diff --git a/docs/SECURITY_HEADERS.md b/docs/SECURITY_HEADERS.md new file mode 100644 index 0000000..234b656 --- /dev/null +++ b/docs/SECURITY_HEADERS.md @@ -0,0 +1,281 @@ +# Security Headers Documentation + +## Overview + +The trivia app implements comprehensive security headers middleware to protect against common web vulnerabilities. All HTTP responses include security headers that enforce best practices for web application security. + +## Implementation + +The security headers are implemented through FastAPI middleware in `backend/core/security_middleware.py` and integrated into the application in `backend/main.py`. + +## Security Headers + +### 1. Strict-Transport-Security (HSTS) + +**Header Value:** `max-age=31536000; includeSubDomains` + +**Purpose:** Forces browsers to use HTTPS connections for all future requests to this domain. + +**Protection Against:** +- Man-in-the-middle attacks +- Protocol downgrade attacks +- Cookie hijacking + +**Details:** +- `max-age=31536000`: Valid for 1 year (31,536,000 seconds) +- `includeSubDomains`: Applies to all subdomains as well + +**Browser Support:** All modern browsers + +**Security Impact:** High - Prevents insecure HTTP connections + +--- + +### 2. Content-Security-Policy (CSP) + +**Header Value:** +``` +default-src 'self'; +script-src 'self' 'unsafe-inline'; +style-src 'self' 'unsafe-inline'; +img-src 'self' data: https:; +font-src 'self' data:; +connect-src 'self' ws: wss:; +``` + +**Purpose:** Restricts which resources can be loaded to prevent Cross-Site Scripting (XSS) attacks. + +**Protection Against:** +- XSS (Cross-Site Scripting) attacks +- Data injection attacks +- Malicious script execution + +**Policy Breakdown:** +- `default-src 'self'`: By default, only allow resources from the same origin +- `script-src 'self' 'unsafe-inline'`: Allow scripts from same origin and inline scripts (needed for modern JavaScript frameworks) +- `style-src 'self' 'unsafe-inline'`: Allow styles from same origin and inline styles +- `img-src 'self' data: https:`: Allow images from same origin, data URIs, and HTTPS sources +- `font-src 'self' data:`: Allow fonts from same origin and data URIs +- `connect-src 'self' ws: wss:`: Allow API connections to same origin and WebSocket connections (required for real-time features) + +**Browser Support:** All modern browsers + +**Security Impact:** Very High - Primary defense against XSS attacks + +**Note:** The CSP policy is configured to work with the trivia app's architecture including WebSocket support for real-time features. Adjust as needed if adding third-party scripts or APIs. + +--- + +### 3. X-Frame-Options + +**Header Value:** `DENY` + +**Purpose:** Prevents the application from being displayed in a frame, iframe, embed, or object tag. + +**Protection Against:** +- Clickjacking attacks +- UI redress attacks +- Invisible iframe overlays + +**Details:** +- `DENY`: The page cannot be displayed in a frame, regardless of the site attempting to do so + +**Browser Support:** All browsers (legacy and modern) + +**Security Impact:** High - Prevents clickjacking attacks + +**Alternative Values:** +- `SAMEORIGIN`: Allow framing only from same origin (not used) +- `ALLOW-FROM uri`: Allow framing from specific URI (deprecated, not used) + +--- + +### 4. X-Content-Type-Options + +**Header Value:** `nosniff` + +**Purpose:** Prevents browsers from MIME-sniffing a response away from the declared content-type. + +**Protection Against:** +- MIME confusion attacks +- Content sniffing vulnerabilities +- Malicious file uploads being executed + +**Details:** +- `nosniff`: Tells browsers to strictly follow the `Content-Type` header and not try to detect the content type + +**Browser Support:** All modern browsers + +**Security Impact:** Medium - Prevents certain types of content-based attacks + +**Example Scenario:** Without this header, a browser might execute a file uploaded as `image.jpg` if it detects JavaScript content inside it. With `nosniff`, the browser respects the declared content type. + +--- + +### 5. X-XSS-Protection + +**Header Value:** `1; mode=block` + +**Purpose:** Enables the browser's built-in XSS filter and instructs it to block the page if an attack is detected. + +**Protection Against:** +- Reflected XSS attacks (legacy protection) + +**Details:** +- `1`: Enable XSS filtering +- `mode=block`: Block the entire page rather than attempting to sanitize + +**Browser Support:** Legacy browsers (modern browsers rely on CSP) + +**Security Impact:** Low (for modern browsers) - CSP is the primary XSS defense + +**Note:** This header is largely obsolete with modern browsers that rely on Content-Security-Policy for XSS protection. However, it provides defense-in-depth for older browsers. + +--- + +## Testing Security Headers + +### Automated Tests + +Comprehensive test suite in `backend/tests/core/test_security_middleware.py` validates: +- All headers are present in all responses +- Header values are correct +- Headers don't break application functionality +- Headers are present on success, error, and authenticated endpoints + +Run tests: +```bash +cd backend +pytest tests/core/test_security_middleware.py -v +``` + +### Manual Testing + +Test headers using curl: +```bash +# Test health endpoint +curl -I http://localhost:8000/health + +# Test API endpoint +curl -I http://localhost:8000/api/v1/auth/login + +# All responses should include the 5 security headers +``` + +Test headers using browser DevTools: +1. Open the application in a browser +2. Open Developer Tools (F12) +3. Navigate to Network tab +4. Load any page +5. Click on a request and view Response Headers +6. Verify all 5 security headers are present + +### Security Scanning + +Use online security header scanners: +- [Security Headers](https://securityheaders.com/) +- [Mozilla Observatory](https://observatory.mozilla.org/) +- [Qualys SSL Labs](https://www.ssllabs.com/ssltest/) + +These tools will analyze your security headers and provide a security rating. + +--- + +## Maintenance and Updates + +### When to Update CSP Policy + +Update the CSP policy when: +- Adding third-party scripts (analytics, CDNs) +- Integrating external APIs +- Adding inline styles or scripts (try to avoid) +- Functionality breaks due to CSP restrictions + +### CSP Policy Best Practices + +1. **Remove 'unsafe-inline'**: Eliminate inline scripts/styles for better security +2. **Use nonce or hash**: For unavoidable inline scripts, use CSP nonces +3. **Monitor violations**: Set up CSP reporting to track policy violations +4. **Test thoroughly**: Changes to CSP can break functionality + +### HSTS Considerations + +- **Production Only**: HSTS should only be enabled in production with proper HTTPS setup +- **Certificate Issues**: Ensure SSL certificates are valid; HSTS will prevent access if HTTPS fails +- **Subdomain Impact**: `includeSubDomains` affects all subdomains +- **HSTS Preload**: Consider adding to the HSTS preload list for maximum protection + +--- + +## Security Improvements + +### Current Implementation: Good ✓ + +The current implementation provides strong protection against common web vulnerabilities. + +### Future Enhancements (Optional): + +1. **Permissions-Policy**: Control browser features (camera, microphone, geolocation) +2. **Referrer-Policy**: Control referrer information sent with requests +3. **CSP Reporting**: Add `report-uri` or `report-to` directives to monitor violations +4. **Subresource Integrity (SRI)**: Add integrity checks for external resources +5. **HSTS Preload**: Submit domain to HSTS preload list + +--- + +## Troubleshooting + +### Issue: CSP blocks WebSocket connections + +**Solution:** Ensure `connect-src 'self' ws: wss:;` is in the CSP policy (already included). + +### Issue: CSP blocks inline styles/scripts + +**Solution:** +1. Move inline code to external files (preferred) +2. Adjust CSP policy to allow specific inline code (less secure) +3. Use CSP nonces for specific inline code (recommended) + +### Issue: Application cannot be embedded in iframe + +**Explanation:** This is intentional. `X-Frame-Options: DENY` prevents clickjacking. + +**Solution:** If legitimate iframe embedding is needed, consider: +1. Change to `X-Frame-Options: SAMEORIGIN` (allow same-origin framing) +2. Use CSP `frame-ancestors` directive for fine-grained control + +### Issue: HSTS prevents access after SSL certificate expires + +**Solution:** +1. Fix the SSL certificate immediately +2. Users must wait for `max-age` to expire or clear HSTS settings +3. Ensure certificates are monitored and renewed before expiration + +--- + +## Compliance + +These security headers help meet compliance requirements for: +- **OWASP Top 10**: Protection against A03:2021 - Injection (XSS) +- **PCI DSS**: Requirement 6.5.7 (XSS prevention) +- **GDPR**: Security measures to protect user data +- **SOC 2**: Security controls for data protection +- **ISO 27001**: Information security best practices + +--- + +## References + +- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) +- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security) +- [Content Security Policy Reference](https://content-security-policy.com/) +- [HTTP Strict Transport Security (HSTS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) +- [Security Headers Best Practices](https://scotthelme.co.uk/hardening-your-http-response-headers/) + +--- + +## Related Documentation + +- [Multi-Tenancy Security](MULTI_TENANCY.md) +- [WebSocket Security](websocket-infrastructure.md) +- [CI/CD Security Scanning](CI_CD.md)