fix: production safety audit — 4 critical bugs across backend, server, and electron#1290
Conversation
…, and electron 1. fix(electron): enforce context isolation in child windows The `open-win` IPC handler created child BrowserWindows with `nodeIntegration: true` and `contextIsolation: false`, while the main window correctly uses `contextIsolation: true`. Any XSS in a child window would gain full Node.js access. Fixed by setting `nodeIntegration: false`, `contextIsolation: true`, and `sandbox: true` to match the main window's security posture. 2. fix(server): separate ExpiredSignatureError from InvalidTokenError in JWT decoding `jwt.decode()` raises `ExpiredSignatureError` (a subclass of `InvalidTokenError`) for expired tokens before the manual `exp` check can execute. The `except InvalidTokenError` block catches both cases and raises `token_invalid`, making it impossible for clients to distinguish expired tokens from corrupted ones. This breaks automatic token refresh flows. Fixed by catching `ExpiredSignatureError` explicitly before `InvalidTokenError`. 3. fix(backend): replace assert statements with proper runtime guards in ListenChatAgent `assert message is not None` and `assert res is not None` in both `step()` and `astep()` are stripped when Python runs with `-O` (optimized mode). This causes silent `None` propagation into the SSE event stream and downstream consumers. Replaced with explicit `if`/`raise RuntimeError` guards that survive optimization and provide clear error messages. 4. fix(backend): cancel background tasks before clearing the set `background_tasks.clear()` in the `improve()` handler only removes references from the set — it does not cancel running asyncio.Task objects. Ghost tasks continue executing, potentially writing stale SSE events into a new conversation's stream. Fixed by iterating and calling `.cancel()` on each non-done task before clearing. Tests: - 4 new tests for assert guard replacements (step/astep, sync/async) - 2 new tests for background task cancellation behavior
| ) | ||
|
|
||
| assert message is not None | ||
| if message is None: |
There was a problem hiding this comment.
Why we change to warning and let the step continue?
There was a problem hiding this comment.
Good point — changed to raise RuntimeError instead, keeping fail-fast behavior consistent with the res=None guard below. Fixed in latest push.
|
|
||
|
|
||
| @pytest.mark.unit | ||
| class TestAssertReplacedWithProperGuards: |
There was a problem hiding this comment.
Just use def test_xxx instead of creating class for now
There was a problem hiding this comment.
Done — replaced class with plain def test_xxx functions.
Address review feedback: - message=None now raises RuntimeError instead of defaulting to empty string, keeping fail-fast behavior consistent with res=None handling - Replace test class with plain def test_xxx functions
|
could @eren-karakus0 link one issue this pr is going to resolve? |
|
Linked - closes #1327. Sorry for the delay! |
When ChatAgent.step/astep returns None, message also remains None, so the message-is-None guard at line 295/408 fires before the res-is-None guard at line 317/430. Update test match patterns from "returned None" to "message is None" to reflect actual flow.
|
Hey @bytecii, just a friendly ping - all review feedback has been addressed and CI is green (7/7). Could you take another look when you get a chance? Thanks! |
|
Friendly ping - just checking if there's anything I should update or adjust in this PR. Happy to address any feedback! |
Related Issue
Closes #1327
Summary
A cross-stack production safety audit uncovering 4 confirmed bugs across the Python backend, server auth, and Electron main process. Each bug is independently verified and includes test coverage where applicable.
Bug 1: Electron Child Windows Missing Context Isolation (CRITICAL)
File:
electron/main/index.ts—open-winIPC handlerThe main window correctly sets
contextIsolation: true, but theopen-winhandler creates childBrowserWindowinstances withnodeIntegration: trueandcontextIsolation: false. With context isolation disabled, any JavaScript running in a child renderer window shares the same context as Electron's built-in modules — meaning any XSS payload or injected script gains fullrequire('child_process').exec(...)access with no sandbox barrier.Before:
After:
Bug 2: JWT Token Expiry Returns Wrong Error Code (HIGH)
File:
server/app/component/auth.py—Auth.decode_token()PyJWT's
jwt.decode()already validates theexpclaim and raisesExpiredSignatureError(a subclass ofInvalidTokenError) for expired tokens. The manualpayload["exp"] < datetime.now()check never executes because expired tokens don't reach that line. Theexcept InvalidTokenErrorblock catches both expired and invalid tokens, raisingtoken_invalidfor both cases.This makes it impossible for clients to distinguish "token expired, please refresh" from "token is corrupted, please re-login", breaking any automatic token refresh logic.
Before:
After:
Bug 3:
assertStatements Used for Runtime Control Flow (HIGH)File:
backend/app/agent/listen_chat_agent.py—step()andastep()Both
step()andastep()useassert message is not Noneandassert res is not Nonefor critical runtime checks. Python'sassertstatements are completely removed when running with-O(optimized mode), which is common in production deployments.When stripped:
Nonemessage propagates into the SSEActionDeactivateAgentDataevent, corrupting the frontend data streamNoneresponse returns to callers, causing downstreamAttributeErrorcrashesBefore:
After:
Bug 4:
background_tasks.clear()Doesn't Cancel Running Tasks (HIGH)File:
backend/app/controller/chat_controller.py—improve()handlerWhen a user sends a follow-up message after task completion,
improve()callstask_lock.background_tasks.clear(). This only removes references from the Python set — it does not cancel the underlyingasyncio.Taskobjects. Ghost tasks continue running in the event loop, holding resources, and potentially writing stale SSE events into the new conversation's stream.Before:
After:
Test Plan
Added 6 new unit tests:
Assert Guard Tests (
test_listen_chat_agent.py):test_step_handles_none_message_without_assert— verifies step() doesn't crash with AssertionErrortest_step_raises_runtime_error_when_res_is_none— verifies RuntimeError instead of AssertionErrortest_astep_handles_none_message_without_assert— async version of message guardtest_astep_raises_runtime_error_when_res_is_none— async version of res guardBackground Task Tests (
test_chat_controller.py):test_improve_cancels_background_tasks_when_done— verifies running tasks are cancelled, done tasks are nottest_improve_handles_missing_background_tasks_attr— verifies graceful handling without the attributeFiles Changed
electron/main/index.tsnodeIntegration: false,contextIsolation: true,sandbox: truein child windowsserver/app/component/auth.pyExpiredSignatureErrorseparately; remove dead manual exp checkbackend/app/agent/listen_chat_agent.pyassertstatements withif/raiseguardsbackend/app/controller/chat_controller.pybackend/tests/app/agent/test_listen_chat_agent.pybackend/tests/app/controller/test_chat_controller.py