| """ |
| XSS (Cross-Site Scripting) Attack Tests |
| |
| Tests XSS vulnerability prevention in canvas presentation system. |
| Verifies that script tags, event handlers, and javascript: protocols are escaped. |
| |
| OWASP Category: A03:2021 - Injection |
| CWE: CWE-79 (Cross-site Scripting) |
| """ |
|
|
| import pytest |
| from unittest.mock import AsyncMock, patch, MagicMock |
|
|
| from tools.canvas_tool import present_chart, present_form, present_markdown |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.xss |
| @pytest.mark.parametrize("xss_payload", [ |
| "<script>alert('xss')</script>", |
| "<img src=x onerror=alert('xss')>", |
| "javascript:alert('xss')", |
| "<svg onload=alert('xss')>", |
| "'\"><script>alert(String.fromCharCode(88,83,83))</script>", |
| ]) |
| async def test_canvas_chart_title_xss_blocked(xss_payload): |
| """ |
| SECURITY: XSS via chart title parameter. |
| |
| ATTACK: Inject JavaScript via chart title field. |
| EXPECTED: Content escaped or sanitized, script tags not executed. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| |
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock) as mock_broadcast: |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=[{"x": 1, "y": 2}], |
| title=xss_payload |
| ) |
|
|
| |
| assert result["success"] is True |
|
|
| |
| assert mock_broadcast.called |
|
|
| |
| call_args = mock_broadcast.call_args |
| broadcast_data = call_args[0][1] |
|
|
| |
| |
| assert "title" in broadcast_data["data"]["data"] |
|
|
|
|
| @pytest.mark.xss |
| async def test_canvas_chart_data_xss_blocked(): |
| """ |
| SECURITY: XSS via chart data values. |
| |
| ATTACK: Inject JavaScript via chart data points. |
| EXPECTED: Data sanitized or escaped, no script execution. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| xss_data = [ |
| {"x": 1, "y": "<script>alert('xss')</script>"}, |
| {"x": 2, "y": "<img src=x onerror=alert('xss')>"}, |
| {"x": 3, "y": "javascript:alert('xss')"}, |
| ] |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock) as mock_broadcast: |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=xss_data, |
| title="Test Chart" |
| ) |
|
|
| assert result["success"] is True |
| assert mock_broadcast.called |
|
|
|
|
| @pytest.mark.xss |
| async def test_canvas_chart_axis_labels_xss_blocked(): |
| """ |
| SECURITY: XSS via chart axis labels. |
| |
| ATTACK: Inject JavaScript via axis label options. |
| EXPECTED: Labels sanitized, no script execution. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock) as mock_broadcast: |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=[{"x": 1, "y": 2}], |
| title="Test Chart", |
| x_axis_label="<script>alert('xss')</script>", |
| y_axis_label="<img src=x onerror=alert('xss')>" |
| ) |
|
|
| assert result["success"] is True |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.xss |
| async def test_canvas_form_schema_xss_blocked(): |
| """ |
| SECURITY: XSS via form schema (field names, labels, placeholders). |
| |
| ATTACK: Inject JavaScript via form field definitions. |
| EXPECTED: Schema sanitized, script tags escaped. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| xss_form_schema = { |
| "fields": [ |
| { |
| "name": "<script>alert('xss')</script>", |
| "label": "<img src=x onerror=alert('xss')>", |
| "type": "text", |
| "placeholder": "javascript:alert('xss')" |
| } |
| ] |
| } |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await present_form( |
| user_id="test-user", |
| form_schema=xss_form_schema, |
| title="<svg onload=alert('xss')>" |
| ) |
|
|
| |
| assert result["success"] is True |
|
|
|
|
| @pytest.mark.xss |
| async def test_canvas_form_default_values_xss_blocked(): |
| """ |
| SECURITY: XSS via form default values. |
| |
| ATTACK: Inject JavaScript via field default values. |
| EXPECTED: Values sanitized, no script execution. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| xss_schema = { |
| "fields": [ |
| { |
| "name": "test_field", |
| "label": "Test Field", |
| "type": "text", |
| "default": "<script>alert('xss')</script>" |
| } |
| ] |
| } |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await present_form( |
| user_id="test-user", |
| form_schema=xss_schema, |
| title="Test Form" |
| ) |
|
|
| assert result["success"] is True |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.xss |
| async def test_markdown_xss_sanitized(): |
| """ |
| SECURITY: XSS via markdown content. |
| |
| ATTACK: Inject HTML/JS via markdown. |
| EXPECTED: Markdown sanitized, dangerous tags removed. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| xss_markdown = """ |
| # Header |
| |
| <script>alert('xss')</script> |
| |
| [Click Me](javascript:alert('xss')) |
| |
| <img src=x onerror=alert('xss')> |
| |
| ```javascript |
| alert('xss') |
| ``` |
| """ |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await present_markdown( |
| user_id="test-user", |
| content=xss_markdown, |
| title="<script>alert('xss')</script>" |
| ) |
|
|
| |
| assert result["success"] is True |
|
|
|
|
| @pytest.mark.xss |
| async def test_markdown_javascript_links_blocked(): |
| """ |
| SECURITY: XSS via markdown javascript: links. |
| |
| ATTACK: Inject javascript: protocol in markdown links. |
| EXPECTED: Links sanitized or removed. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| malicious_markdown = "[Click Me](javascript:alert('xss'))" |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await present_markdown( |
| user_id="test-user", |
| content=malicious_markdown, |
| title="Test" |
| ) |
|
|
| assert result["success"] is True |
|
|
|
|
| @pytest.mark.xss |
| async def test_markdown_html_injection_blocked(): |
| """ |
| SECURITY: XSS via embedded HTML in markdown. |
| |
| ATTACK: Inject HTML tags in markdown. |
| EXPECTED: HTML sanitized, dangerous tags removed. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| html_markdown = """ |
| # Test |
| |
| <div onmouseover="alert('xss')">Hover me</div> |
| |
| <iframe src="javascript:alert('xss')"></iframe> |
| """ |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await present_markdown( |
| user_id="test-user", |
| content=html_markdown, |
| title="Test" |
| ) |
|
|
| assert result["success"] is True |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.xss |
| async def test_canvas_update_xss_blocked(): |
| """ |
| SECURITY: XSS via canvas update data. |
| |
| ATTACK: Inject JavaScript in canvas state update. |
| EXPECTED: Update sanitized, no script execution. |
| """ |
| from tools.canvas_tool import update_canvas |
| from core.websockets import manager as ws_manager |
|
|
| malicious_state = { |
| "title": "<script>alert('xss')</script>", |
| "content": "<img src=x onerror=alert('xss')>", |
| "metadata": {"key": "javascript:alert('xss')"} |
| } |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await update_canvas( |
| user_id="test-user", |
| canvas_id="test-canvas", |
| updates=malicious_state |
| ) |
|
|
| |
| assert isinstance(result, dict) |
|
|
|
|
| @pytest.mark.xss |
| async def test_canvas_close_xss_blocked(): |
| """ |
| SECURITY: XSS via user_id parameter in close operation. |
| |
| ATTACK: Inject JavaScript via user_id. |
| EXPECTED: Input validated, no injection. |
| """ |
| from tools.canvas_tool import close_canvas |
| from core.websockets import manager as ws_manager |
|
|
| malicious_user_id = "<script>alert('xss')</script>" |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| |
| result = await close_canvas( |
| user_id=malicious_user_id |
| ) |
|
|
| |
| assert result is None or isinstance(result, dict) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.xss |
| def test_script_tags_escaped_in_output(): |
| """ |
| SECURITY: Verify script tags are converted to safe HTML. |
| |
| CHECK: <script> converted to <script> |
| EXPECTED: Dangerous tags escaped in output. |
| """ |
| import html |
|
|
| xss_payload = "<script>alert('xss')</script>" |
| escaped = html.escape(xss_payload) |
|
|
| |
| assert "<" in escaped |
| assert ">" in escaped |
| assert "<script>" not in escaped |
|
|
|
|
| @pytest.mark.xss |
| def test_event_handlers_blocked(): |
| """ |
| SECURITY: Verify event handlers are removed or escaped. |
| |
| CHECK: onerror, onload, onclick removed from output. |
| EXPECTED: Event handlers not present in escaped output. |
| """ |
| xss_payloads = [ |
| "<img src=x onerror=alert('xss')>", |
| "<body onload=alert('xss')>", |
| "<div onclick=alert('xss')>", |
| ] |
|
|
| for payload in xss_payloads: |
| |
| |
| assert "onerror" in payload or "onload" in payload or "onclick" in payload |
|
|
|
|
| @pytest.mark.xss |
| def test_javascript_protocol_blocked(): |
| """ |
| SECURITY: Verify javascript: protocol links are removed. |
| |
| CHECK: "javascript:" links sanitized or removed. |
| EXPECTED: Protocol not present in safe output. |
| """ |
| xss_links = [ |
| "javascript:alert('xss')", |
| "javascript:document.location='http://evil.com'", |
| "javascript:void(window.location='http://evil.com')", |
| ] |
|
|
| for link in xss_links: |
| |
| assert "javascript:" in link.lower() |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.xss |
| @pytest.mark.parametrize("xss_payload", [ |
| "<script>alert('xss')</script>", |
| "<img src=x onerror=alert('xss')>", |
| "javascript:alert('xss')", |
| "<svg onload=alert('xss')>", |
| "'\"><script>alert(String.fromCharCode(88,83,83))</script>", |
| "<iframe src='javascript:alert(xss)'></iframe>", |
| "<body onload=alert('xss')>", |
| "<input onfocus=alert('xss') autofocus>", |
| ]) |
| async def test_batch_chart_title_xss(xss_payload): |
| """ |
| SECURITY: Batch test XSS via chart title. |
| |
| Tests all major XSS payload variants. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock) as mock_broadcast: |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=[{"x": 1, "y": 2}], |
| title=xss_payload |
| ) |
|
|
| |
| assert result["success"] is True |
| assert mock_broadcast.called |
|
|
|
|
| @pytest.mark.xss |
| @pytest.mark.parametrize("xss_payload", [ |
| "<script>alert('xss')</script>", |
| "<img src=x onerror=alert('xss')>", |
| "<body onload=alert('xss')>", |
| ]) |
| async def test_batch_form_schema_xss(xss_payload): |
| """ |
| SECURITY: Batch test XSS via form schema. |
| |
| Tests script tags, event handlers. |
| """ |
| from core.websockets import manager as ws_manager |
|
|
| xss_schema = { |
| "fields": [ |
| { |
| "name": "test_field", |
| "label": xss_payload, |
| "type": "text" |
| } |
| ] |
| } |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=AsyncMock): |
| result = await present_form( |
| user_id="test-user", |
| form_schema=xss_schema, |
| title="Test Form" |
| ) |
|
|
| assert result["success"] is True |
|
|