fix: close remaining security gaps, test fixes, frontend integration, event bus
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- RCE: move _check_dangerous_imports() BEFORE exec_module() in plugins.py - verify_ws_origin: reject empty Origin header when CORS configured - Test: ai_app fixture with permission_registry init for ai_assistant - Test: login_client sets CSRF token + Origin as client default headers - Test: SESSION_COOKIE_SECURE=false override + get_settings.cache_clear() - Test: asyncio_default_test_loop_scope=session fixes event loop closed - Test: fix 15 assertions (paths, variables, auth expectations) - Frontend: integrate SavedFilterBar in ContactsList, Mail, Calendar - Frontend: integrate TagSelector in ContactsList, Mail, Calendar - Event Bus: add 4 subscribers in system_notif (conversation/participant/reaction) - Docs: update all analysis reports and FIX-PLAN-V2 to current state
This commit is contained in:
+4
-1
@@ -104,7 +104,10 @@ def verify_ws_origin(websocket) -> bool:
|
||||
return True
|
||||
origin = websocket.headers.get("origin", "")
|
||||
if not origin:
|
||||
return True # Non-browser clients don't send Origin
|
||||
# Non-browser clients (curl, etc.) don't send Origin.
|
||||
# Reject when CORS is configured — WebSocket should come from a browser.
|
||||
logger.warning("WebSocket connection rejected: missing Origin header")
|
||||
return False
|
||||
return origin in allowed_origins
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ class SystemNotifPlugin(BasePlugin):
|
||||
"notification.created",
|
||||
"backup.completed",
|
||||
"backup.failed",
|
||||
"conversation.created",
|
||||
"participant.joined",
|
||||
"participant.left",
|
||||
"reaction.added",
|
||||
],
|
||||
migrations=[],
|
||||
permissions=["system_notif:read"],
|
||||
@@ -128,6 +132,22 @@ class SystemNotifPlugin(BasePlugin):
|
||||
"""Handle backup.failed event → system message (error)."""
|
||||
await self._create_system_notification(payload, event_type="backup.failed", severity="error")
|
||||
|
||||
async def on_conversation_created(self, payload: dict[str, Any]) -> None:
|
||||
"""Handle conversation.created event → system message."""
|
||||
await self._create_system_notification(payload, event_type="conversation.created")
|
||||
|
||||
async def on_participant_joined(self, payload: dict[str, Any]) -> None:
|
||||
"""Handle participant.joined event → system message."""
|
||||
await self._create_system_notification(payload, event_type="participant.joined")
|
||||
|
||||
async def on_participant_left(self, payload: dict[str, Any]) -> None:
|
||||
"""Handle participant.left event → system message."""
|
||||
await self._create_system_notification(payload, event_type="participant.left")
|
||||
|
||||
async def on_reaction_added(self, payload: dict[str, Any]) -> None:
|
||||
"""Handle reaction.added event → system message."""
|
||||
await self._create_system_notification(payload, event_type="reaction.added")
|
||||
|
||||
async def _create_system_notification(
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
@@ -173,6 +193,10 @@ class SystemNotifPlugin(BasePlugin):
|
||||
"notification.created": "Benachrichtigung",
|
||||
"backup.completed": "Backup erfolgreich",
|
||||
"backup.failed": "Backup fehlgeschlagen",
|
||||
"conversation.created": "Neue Konversation",
|
||||
"participant.joined": "Teilnehmer beigetreten",
|
||||
"participant.left": "Teilnehmer verlassen",
|
||||
"reaction.added": "Reaktion hinzugefügt",
|
||||
}
|
||||
title = event_titles.get(event_type, event_type)
|
||||
|
||||
|
||||
@@ -383,7 +383,15 @@ def _extract_plugin_from_zip(zip_path: str) -> tuple[Path, str, type[BasePlugin]
|
||||
if plugin_py_path is None:
|
||||
raise ValueError("ZIP does not contain a plugin.py file")
|
||||
|
||||
# Import the module dynamically
|
||||
# Security: check source code BEFORE executing it
|
||||
source_code = plugin_py_path.read_text(encoding="utf-8")
|
||||
dangerous = _check_dangerous_imports(source_code)
|
||||
if dangerous:
|
||||
raise ValueError(
|
||||
f"Plugin contains dangerous patterns: {', '.join(dangerous)}"
|
||||
)
|
||||
|
||||
# Import the module dynamically (safe — source validated above)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"uploaded_plugin", plugin_py_path
|
||||
)
|
||||
@@ -412,14 +420,6 @@ def _extract_plugin_from_zip(zip_path: str) -> tuple[Path, str, type[BasePlugin]
|
||||
# Validate plugin name
|
||||
_validate_manifest_name(manifest.name)
|
||||
|
||||
# Check for dangerous imports in plugin.py
|
||||
source_code = plugin_py_path.read_text(encoding="utf-8")
|
||||
dangerous = _check_dangerous_imports(source_code)
|
||||
if dangerous:
|
||||
raise ValueError(
|
||||
f"Plugin contains dangerous patterns: {', '.join(dangerous)}"
|
||||
)
|
||||
|
||||
# Check migration SQL files
|
||||
migrations_dir = plugin_py_path.parent / "migrations"
|
||||
if migrations_dir.exists():
|
||||
|
||||
Reference in New Issue
Block a user