"""
Canvas CSS security integration tests (INTG-15).
Tests cover:
- CSS sanitization
- Dangerous URL blocking
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from tests.factories.canvas_factory import CanvasAuditFactory
from tests.factories.agent_factory import AutonomousAgentFactory
from core.models import CanvasAudit
import uuid
DANGEROUS_CSS_PATTERNS = [
"body { background: url('javascript:alert(1)'); }",
"div { behavior: url(xss.htc); }",
"a { binding: url('javascript:alert(1)'); }",
"li { list-style-image: url('javascript:alert(1)'); }",
"@import url('javascript:alert(1)');",
"@font-face { src: url('javascript:alert(1)'); }",
"body { -moz-binding: url('javascript:alert(1)'); }",
"div { background-image: url('javascript:alert(1)'); }",
]
SAFE_CSS_PATTERNS = [
".container { padding: 10px; }",
"div { color: red; }",
".label { font-size: 14px; }",
"#main { width: 100%; max-width: 1200px; }",
"button { background-color: blue; border: none; }",
".flex { display: flex; justify-content: center; }",
]
class TestCSSSanitization:
"""Test CSS sanitization."""
def test_javascript_url_removed(self, client: TestClient, auth_token: str, db_session: Session):
"""Test javascript: URLs are removed from CSS."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
dangerous_css = "body { background: url('javascript:alert(1)'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
# Should remove or block javascript: URLs
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
assert "javascript:" not in css.lower()
def test_behavior_removed(self, client: TestClient, auth_token: str, db_session: Session):
"""Test behavior property is removed (IE XSS)."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
dangerous_css = "div { behavior: url(xss.htc); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Behavior property should be removed
assert "behavior:" not in css.lower()
def test_expression_removed(self, client: TestClient, auth_token: str, db_session: Session):
"""Test CSS expressions are removed."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
dangerous_css = "div { width: expression(alert(1)); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Expression should be removed
assert "expression(" not in css.lower()
def test_binding_removed(self, client: TestClient, auth_token: str, db_session: Session):
"""Test CSS binding property is removed."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
dangerous_css = "a { binding: url('javascript:alert(1)'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Binding should be removed
assert "binding:" not in css.lower()
@pytest.mark.parametrize("safe_css", SAFE_CSS_PATTERNS)
def test_safe_css_preserved(self, client: TestClient, auth_token: str, safe_css, db_session: Session):
"""Test safe CSS is preserved."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": safe_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
# Safe CSS should be allowed
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Safe properties should be preserved
assert len(css) > 0
class TestDangerousURLBlocking:
"""Test blocking of dangerous URLs in CSS."""
@pytest.mark.parametrize("dangerous_css", DANGEROUS_CSS_PATTERNS)
def test_dangerous_urls_blocked(self, client: TestClient, auth_token: str, dangerous_css, db_session: Session):
"""Test various dangerous URL patterns are blocked."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
# Should block or sanitize dangerous URLs
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Check dangerous patterns are removed
assert "javascript:" not in css.lower()
assert "behavior:" not in css.lower()
assert "binding:" not in css.lower()
def test_data_url_blocked(self, client: TestClient, auth_token: str, db_session: Session):
"""Test data: URLs are blocked in CSS."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
data_url_css = "body { background: url('data:image/svg+xml,'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": data_url_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# data: URLs should be removed or sanitized
assert "data:" not in css.lower() or "data:image" not in css.lower()
def test_vbscript_blocked(self, client: TestClient, auth_token: str, db_session: Session):
"""Test vbscript: URLs are blocked."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
vbscript_css = "body { background: url('vbscript:msgbox(1)'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": vbscript_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# vbscript: should be blocked
assert "vbscript:" not in css.lower()
class TestCSSAtRules:
"""Test CSS @-rules security."""
def test_dangerous_import_blocked(self, client: TestClient, auth_token: str, db_session: Session):
"""Test dangerous @import rules are blocked."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
import_css = "@import url('javascript:alert(1)');"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": import_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Dangerous @import should be blocked
assert "@import" not in css or "javascript:" not in css.lower()
def test_safe_import_allowed(self, client: TestClient, auth_token: str, db_session: Session):
"""Test safe @import rules are allowed."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
import_css = "@import url('https://cdn.example.com/styles.css');"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": import_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
# Safe @import may be allowed
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Should preserve or validate
assert isinstance(css, str)
def test_font_face_security(self, client: TestClient, auth_token: str, db_session: Session):
"""Test @font-face security."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
font_css = "@font-face { src: url('javascript:alert(1)'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": font_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Dangerous font URLs should be blocked
assert "javascript:" not in css.lower()
class TestCSSPropertyFiltering:
"""Test CSS property filtering."""
def test_safe_properties_allowed(self, client: TestClient, auth_token: str, db_session: Session):
"""Test safe CSS properties are allowed."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
safe_css = """
.container {
padding: 10px;
margin: 20px;
color: #333;
background-color: #fff;
font-size: 14px;
display: flex;
}
"""
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": safe_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
# Safe properties should be preserved
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Check properties preserved
assert "padding:" in css
assert "color:" in css
assert "display:" in css
def test_moz_binding_blocked(self, client: TestClient, auth_token: str, db_session: Session):
"""Test -moz-binding is blocked."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
binding_css = "div { -moz-binding: url('javascript:alert(1)'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": binding_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# -moz-binding should be blocked
assert "-moz-binding:" not in css.lower()
class TestCSSAuditLogging:
"""Test CSS security audit logging."""
def test_dangerous_css_logged(self, client: TestClient, auth_token: str, db_session: Session):
"""Test dangerous CSS attempts are logged."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
dangerous_css = "body { background: url('javascript:alert(1)'); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
# Check security event logged
audits = db_session.query(CanvasAudit).filter(
CanvasAudit.id == canvas_id
).all()
assert len(audits) >= 0
def test_sanitization_metadata(self, client: TestClient, auth_token: str, db_session: Session):
"""Test CSS sanitization metadata is logged."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
dangerous_css = "div { behavior: url(xss.htc); }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": dangerous_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
# Check sanitization recorded
audits = db_session.query(CanvasAudit).filter(
CanvasAudit.id == canvas_id
).all()
if audits and audits[0].audit_metadata:
# Should include sanitization details
metadata = audits[0].audit_metadata
assert isinstance(metadata, dict)
class TestCSSEscaping:
"""Test CSS content escaping."""
def test_css_content_escaped(self, client: TestClient, auth_token: str, db_session: Session):
"""Test CSS content property is escaped."""
agent = AutonomousAgentFactory()
db_session.add(agent)
db_session.commit()
canvas_id = str(uuid.uuid4())
content_css = "div:before { content: ''; }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": content_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Content should be escaped
assert " */ div { color: red; }"
response = client.post(
f"/api/canvas/{canvas_id}/components",
json={
"type": "custom",
"css": comment_css
},
headers={"Authorization": f"Bearer {auth_token}"}
)
if response.status_code in [200, 201]:
data = response.json()
css = data.get("css", "")
# Dangerous content in comments should be handled
assert isinstance(css, str)