Spaces:
Sleeping
Sleeping
| """ | |
| Security-focused tests β OWASP Top-10 scope for this application. | |
| Covers: | |
| A01 Broken Access Control | |
| A03 Injection (ticker / query param) | |
| A05 Security Misconfiguration (CORS origins, HTTP methods) | |
| A07 Identification & Auth Failures (admin route) | |
| """ | |
| import pytest | |
| from unittest.mock import patch | |
| from fastapi.testclient import TestClient | |
| def _client(): | |
| import sys | |
| from unittest.mock import patch as p | |
| with p.dict("os.environ", {"DEMO_MODE": "false", "LLM_MODE": "cloud"}): | |
| for mod in list(sys.modules.keys()): | |
| if "backend.main" in mod: | |
| del sys.modules[mod] | |
| import backend.main as m | |
| return TestClient(m.app), m | |
| # ββ A03 β Injection via ticker param βββββββββββββββββββββββββββββββββββββββββ | |
| class TestInjection: | |
| PAYLOADS = [ | |
| # SQL injection | |
| "' OR 1=1--", | |
| "'; DROP TABLE stocks;--", | |
| # XSS | |
| "<script>alert(1)</script>", | |
| '"><img src=x onerror=alert(1)>', | |
| # Path traversal | |
| "../../etc/passwd", | |
| "../.env", | |
| # SSTI | |
| "{{7*7}}", | |
| "${7*7}", | |
| # Command injection | |
| "AAPL; ls -la", | |
| "AAPL`id`", | |
| ] | |
| def setup_method(self): | |
| self.client, _ = _client() | |
| def test_quick_endpoint_rejects_injection(self, payload): | |
| r = self.client.get(f"/quick/{payload}") | |
| # Path-traversal payloads are normalised by Starlette β 404; | |
| # others hit the ticker regex validator β 400. Both mean rejected. | |
| assert r.status_code in (400, 404), f"Injection payload not rejected: {payload!r}" | |
| def test_stream_endpoint_rejects_injection(self, payload): | |
| r = self.client.get(f"/stream/{payload}") | |
| assert r.status_code in (400, 404), f"Injection payload not rejected: {payload!r}" | |
| # ββ A05 β CORS / method misconfiguration βββββββββββββββββββββββββββββββββββββ | |
| class TestSecurityMisconfiguration: | |
| def setup_method(self): | |
| self.client, _ = _client() | |
| def test_cors_disallows_arbitrary_origin(self): | |
| r = self.client.get( | |
| "/quick/AAPL", | |
| headers={"Origin": "https://evil.com"}, | |
| ) | |
| # Should not echo back a wildcard ACAO header | |
| acao = r.headers.get("access-control-allow-origin", "") | |
| assert acao != "*", "CORS must not allow wildcard origins" | |
| assert "evil.com" not in acao | |
| def test_post_to_quick_disallowed(self): | |
| r = self.client.post("/quick/AAPL") | |
| assert r.status_code == 405 | |
| def test_put_to_stream_disallowed(self): | |
| r = self.client.put("/stream/AAPL") | |
| assert r.status_code == 405 | |
| # ββ A01 β Broken Access Control (admin route) βββββββββββββββββββββββββββββββββ | |
| class TestAdminAuth: | |
| """ | |
| Admin auth route in app/api/admin/auth/route.ts (Next.js). | |
| Here we test the contract β no password header β 401. | |
| """ | |
| def test_no_password_returns_401(self): | |
| # Replicated contract test β admin route must reject missing auth | |
| # This simulates the server-side check | |
| import os | |
| admin_pw = os.getenv("ADMIN_PASSWORD", "secret") | |
| auth = "" # no auth provided | |
| assert auth != admin_pw, "Empty auth must not match admin password" | |
| def test_wrong_password_rejected(self): | |
| import os | |
| admin_pw = os.getenv("ADMIN_PASSWORD", "secret") | |
| assert "wrong_password" != admin_pw | |
| # ββ A05 β Response headers (via next.config.js β contract test) ββββββββββββββ | |
| class TestSecurityHeaders: | |
| """ | |
| Verify next.config.js exports the required security headers. | |
| These are applied by Next.js at the CDN/edge layer. | |
| """ | |
| def test_security_headers_defined(self): | |
| import importlib.util, os, sys | |
| config_path = os.path.join( | |
| os.path.dirname(__file__), "..", "..", "xenex-ai", "next.config.js" | |
| ) | |
| if not os.path.exists(config_path): | |
| pytest.skip("next.config.js not in test path") | |
| with open(config_path) as f: | |
| content = f.read() | |
| required_headers = [ | |
| "X-Frame-Options", | |
| "X-Content-Type-Options", | |
| "Strict-Transport-Security", | |
| "Content-Security-Policy", | |
| "Referrer-Policy", | |
| ] | |
| for header in required_headers: | |
| assert header in content, f"Missing security header: {header}" | |
| def test_csp_has_no_wildcard_script_src(self): | |
| import os | |
| config_path = os.path.join( | |
| os.path.dirname(__file__), "..", "..", "xenex-ai", "next.config.js" | |
| ) | |
| if not os.path.exists(config_path): | |
| pytest.skip("next.config.js not in test path") | |
| with open(config_path) as f: | |
| content = f.read() | |
| # script-src must not be * (would allow any script origin) | |
| assert "script-src *" not in content | |