fix: critical security hardening (Phase A permissions audit)
- Remove role field from UserCreate schema, hardcode 'engineer' at registration
- Escape all user content in HTML export with html.escape() (XSS fix)
- Add field_validator to reject default SECRET_KEY when DEBUG=False
- Add CHECK constraint on users.role ('engineer'|'viewer') + migration 011
- Fix test_admin fixture to properly grant is_super_admin via ORM
- Fix circular FK (users↔invite_codes) in test DB setup with DROP SCHEMA CASCADE
- Add 5 new security tests (role validation + XSS prevention)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -603,3 +603,84 @@ class TestSessions:
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
assert "Evidence / Reference" not in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_html_export_escapes_script_tags(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Test that HTML export escapes script tags in user content (XSS prevention)."""
|
||||
session_data = {
|
||||
"tree_id": test_tree["id"],
|
||||
"ticket_number": '<script>alert("xss")</script>',
|
||||
"client_name": '<img onerror="alert(1)" src=x>'
|
||||
}
|
||||
create_response = await client.post(
|
||||
"/api/v1/sessions", json=session_data, headers=auth_headers
|
||||
)
|
||||
session_id = create_response.json()["id"]
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/sessions/{session_id}/export",
|
||||
json={"format": "html", "include_tree_info": True},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
assert '<script>' not in content
|
||||
assert '<script>' in content
|
||||
assert '<img' in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_html_export_escapes_special_chars(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Test that HTML export properly escapes special characters."""
|
||||
session_data = {
|
||||
"tree_id": test_tree["id"],
|
||||
"ticket_number": 'TICK-001 <b>"bold"</b> & \'quoted\''
|
||||
}
|
||||
create_response = await client.post(
|
||||
"/api/v1/sessions", json=session_data, headers=auth_headers
|
||||
)
|
||||
session_id = create_response.json()["id"]
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/sessions/{session_id}/export",
|
||||
json={"format": "html", "include_tree_info": True},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
assert '&' in content
|
||||
assert '<b>' in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_html_export_escapes_scratchpad(
|
||||
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
||||
):
|
||||
"""Test that HTML export escapes scratchpad content."""
|
||||
create_response = await client.post(
|
||||
"/api/v1/sessions",
|
||||
json={"tree_id": test_tree["id"]},
|
||||
headers=auth_headers
|
||||
)
|
||||
session_id = create_response.json()["id"]
|
||||
|
||||
await client.patch(
|
||||
f"/api/v1/sessions/{session_id}/scratchpad",
|
||||
json={"scratchpad": '<script>document.cookie</script>'},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/sessions/{session_id}/export",
|
||||
json={"format": "html"},
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
assert '<script>' not in content
|
||||
assert '<script>' in content
|
||||
|
||||
Reference in New Issue
Block a user