fix(security): workspace path allowlist + session ownership (#655)#705
Conversation
Prevent authenticated cross-tenant RCE by constraining every client-supplied workspace path to a configurable allowlist (WORKSPACE_ROOT), and binding interactive sessions to their creating user. - enforce_workspace_allowlist() (ui/dependencies.py): resolved path must be within a WORKSPACE_ROOT entry; '..' escapes collapse via resolve(). In HOSTED mode the allowlist is mandatory (fail closed) and each user is confined to <root>/<user_id>. No-op when WORKSPACE_ROOT is unset (single-operator default). - Applied at all client-supplied-path funnels: get_v2_workspace (REST), POST /api/v2/workspaces (init), and POST /api/v2/sessions (create). - interactive_sessions gains a user_id column (+ idempotent ALTER migration); create_session persists the authed user. terminal_ws/session_chat_ws now fail closed: when auth is enabled an ownerless or mismatched session is rejected. Closes the RCE foundation; remaining hosted multi-tenancy hardening (session REST owner-scoping, TOCTOU symlink isolation) tracked as follow-up.
|
Warning Review limit reached
More reviews will be available in 37 minutes and 41 seconds. Learn how PR review limits work. Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR hardens v2 API security by adding a ChangesSession ownership and workspace allowlist enforcement
Sequence Diagram(s)sequenceDiagram
participant Client
participant create_session
participant enforce_workspace_allowlist
participant InteractiveSessionRepository
rect rgba(100, 149, 237, 0.5)
note over Client,InteractiveSessionRepository: Session Creation with Allowlist
Client->>create_session: POST /api/v2/sessions (workspace_path, JWT)
create_session->>create_session: require_auth → user_id
create_session->>enforce_workspace_allowlist: Path(workspace_path), user_id
alt path outside allowlist or missing WORKSPACE_ROOT/user_id
enforce_workspace_allowlist-->>Client: HTTP 403 / 500
else path accepted
enforce_workspace_allowlist-->>create_session: resolved_path
create_session->>InteractiveSessionRepository: repo.create(resolved_path, user_id=user_id)
InteractiveSessionRepository-->>create_session: session record
create_session-->>Client: 201 Created
end
end
sequenceDiagram
participant WSClient
participant session_chat_ws
participant _authenticate_websocket
participant interactive_sessions_repo
rect rgba(144, 238, 144, 0.5)
note over WSClient,interactive_sessions_repo: WebSocket Ownership Check
WSClient->>session_chat_ws: WS connect (session_id)
session_chat_ws->>_authenticate_websocket: authenticate
_authenticate_websocket-->>session_chat_ws: authenticated=True, user_id=42
session_chat_ws->>interactive_sessions_repo: get(session_id)
interactive_sessions_repo-->>session_chat_ws: session (session_user_id=99)
alt session_user_id missing or != user_id
session_chat_ws-->>WSClient: close(1008, "Forbidden: session belongs to another user")
else ownership confirmed
session_chat_ws-->>WSClient: WS accepted, stream begins
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Security Fix Review — Workspace Path Allowlist + Session Ownership (#655)First review — no prior comments to reconcile. This PR patches a genuine, high-severity vulnerability: an authenticated user could supply an arbitrary What's Working Well
Issues1. Circular import via local import (fragile)
2. Type inconsistency on the
3. WebSocket close codes are inconsistent
4. with pytest.raises(Exception):
with client.websocket_connect("/ws/sessions/s1/chat?token=x"):
passThis passes if the server raises any exception including an unrelated internal error. Asserting the specific Minor Notes
SummaryThe vulnerability fix is correct and well-structured. Issues 2–4 are minor polish; item 1 (circular import) is the only thing worth resolving before merge since lazy imports are silent failure modes in future refactors. Verdict: Approve pending item 1. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
codeframe/platform_store/schema_manager.py (1)
190-198: 💤 Low valueSQLite
ALTER TABLE ADD COLUMNdoes not enforce theREFERENCESconstraint for existing rows.The
REFERENCES users(id)clause in theALTER TABLEstatement is parsed by SQLite but is not retroactively enforced on existing rows, and foreign key checks only apply to new inserts/updates whenPRAGMA foreign_keys=ON. This is fine for this use case since:
- Existing rows get
NULL(no enforcement needed)- New rows go through the repository which validates
user_idupstreamHowever, consider adding an index on
user_idin_create_indexes()if ownership lookups become frequent (e.g., listing sessions by user).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@codeframe/platform_store/schema_manager.py` around lines 190 - 198, Consider adding an index on the user_id column for the interactive_sessions table to optimize ownership lookups if they become frequent. Locate the _create_indexes() method and evaluate whether to add an index on the user_id column of the interactive_sessions table. This is an optional optimization that should be added if queries frequently filter or join on user_id for listing sessions by user, but is not required since the ALTER TABLE ADD COLUMN statement already establishes the foreign key relationship properly despite SQLite not retroactively enforcing it on existing rows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/api/test_session_chat_ws.py`:
- Around line 74-76: The test currently catches the generic Exception when
testing the websocket connection with an invalid token. Replace the generic
Exception with WebSocketDisconnect to assert the specific websocket failure.
Since the router explicitly closes with code 1008 for ownership mismatches,
verify that the WebSocketDisconnect exception includes the expected close code
1008. Reference the existing test patterns in the same test_session_chat_ws.py
file that already validate WebSocketDisconnect with the proper close code to
ensure consistency with the established testing approach.
---
Nitpick comments:
In `@codeframe/platform_store/schema_manager.py`:
- Around line 190-198: Consider adding an index on the user_id column for the
interactive_sessions table to optimize ownership lookups if they become
frequent. Locate the _create_indexes() method and evaluate whether to add an
index on the user_id column of the interactive_sessions table. This is an
optional optimization that should be added if queries frequently filter or join
on user_id for listing sessions by user, but is not required since the ALTER
TABLE ADD COLUMN statement already establishes the foreign key relationship
properly despite SQLite not retroactively enforcing it on existing rows.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 170bc0bc-fc1b-4e84-adf3-93796163c853
📒 Files selected for processing (9)
codeframe/platform_store/repositories/interactive_sessions.pycodeframe/platform_store/schema_manager.pycodeframe/ui/dependencies.pycodeframe/ui/routers/interactive_sessions_v2.pycodeframe/ui/routers/session_chat_ws.pycodeframe/ui/routers/terminal_ws.pycodeframe/ui/routers/workspace_v2.pytests/api/test_session_chat_ws.pytests/ui/test_workspace_allowlist.py
…smatch (#655) CodeRabbit: tighten the broad pytest.raises(Exception) to the specific WebSocketDisconnect with the policy-violation close code.
|
Thanks @coderabbitai — addressed in 95c2a04:
Broader hosted multi-tenancy hardening (session REST owner-scoping, TOCTOU symlink isolation) is tracked in #704. |
|
Tip For best results, initiate chat on the files or code changes.
The security-critical paths are well-guarded. Looking forward to seeing the session REST owner-scoping and TOCTOU symlink hardening land in |
Follow-up Review (95c2a04 update)Checking items from my prior review. Fixed
Still OpenItem 1 - Circular import (was my stated blocker for merge) The lazy import inside def enforce_workspace_allowlist(path, user_id):
# Local import avoids a circular import (server -> routers -> dependencies).
from codeframe.ui.server import is_hosted_modeThis pattern is fragile: if the import graph changes, it fails silently at call time instead of at startup. Item 2 - Type inconsistency (minor)
Item 3 - WebSocket close codes (minor)
Item 5 - Still present in Path to merge: Fix the circular import (item 1) — that is the only structural concern I flagged. Items 2, 3, and 5 can follow in a cleanup PR or be addressed here if convenient. |
|
Follow-up Review -- Workspace Allowlist + Session Ownership (#655) Continuing from my prior review. The diff has not changed since that pass, so the three outstanding items remain open. A correction to my previous review is noted below. Outstanding Item 1 -- Circular import (still open / blocker) enforce_workspace_allowlist in dependencies.py still lazy-imports is_hosted_mode from server.py on every call. This works until anyone refactors the import graph, at which point it silently breaks at call-time rather than import-time. Extracting is_hosted_mode into codeframe/lib/deployment.py (a two-line function reading CODEFRAME_DEPLOYMENT_MODE) dissolves the cycle. This remains the only blocker. Outstanding Item 2 -- Type annotation inconsistency (minor) workspace_v2.py:229 declares Outstanding Item 3 -- Inconsistent WebSocket close codes (minor) terminal_ws.py closes with 4003 (custom), session_chat_ws.py closes with 1008 (RFC 6455 Policy Violation) for the same enforcement reason. 1008 is semantically correct for both; terminal_ws.py should be aligned. Correction to Item 4 from prior review I previously said test_chat_ws_ownership_mismatch_closes catches bare Exception. That was wrong. The test uses One new observation The _create_session helper in test_session_chat_ws.py imports TestClient at module level and again inside the new test function body. Redundant local re-import -- not a correctness issue, just cleanup. Summary: security logic is correct and well-tested. Item 1 (circular import) is the only blocker. Items 2 and 3 are minor polish. Item 4 from the prior review was already handled correctly. |
Closes #655.
Problem
get_v2_workspaceresolved a client-suppliedworkspace_pathand only checked for a.codeframemarker — no allowlist. Combined with the ReAct agent'srun_command(shell=True) and workspace-scoped file tools, an authenticated user could point operations at any host directory: authenticated cross-tenant RCE the moment the server serves >1 user or runs inHOSTEDmode.Fix
A single shared gate,
enforce_workspace_allowlist(path, user_id)(ui/dependencies.py), applied at every client-supplied-path funnel:get_v2_workspace(20 v2 routers)POST /api/v2/workspacesPOST /api/v2/sessionsWORKSPACE_ROOTentry (os.pathsep-separated)...escapes collapse viaresolve().WORKSPACE_ROOT): no-op — single-operator workflow is unchanged.<root>/<user_id>so one tenant can't reach another's subtree.Session ownership
interactive_sessionsgains auser_idcolumn (+ idempotentALTERmigration for existing DBs).create_sessionpersists the authenticated user.terminal_ws/session_chat_wsnow fail closed: when auth is enabled, an ownerless (NULL) or mismatched session is rejected (the terminal ownership check existed but was dead code — no session ever carried an owner).Acceptance criteria
Tests
tests/ui/test_workspace_allowlist.py— allowlist (inside/outside/traversal), hosted mandatory-root, per-user confinement, authed-user-required, session-create rejection + resolved-path storage + owner persistence, workspace-init rejection.tests/api/test_session_chat_ws.py::test_chat_ws_ownership_mismatch_closes.Known Limitations (tracked in #704 — gate hosted launch on it)
Cross-family review surfaced two further hardening items, broader than #655 and HOSTED-only (not deployed):
Review: cross-family (
codex review) primary — drove the session-create, workspace-init, and session-ownership additions above.Summary by CodeRabbit
New Features
Bug Fixes
Chores