[security] reject blocked roots for remote workspaces - #3731
Hinotoi-agent wants to merge 2 commits into
Conversation
|
| Filename | Overview |
|---|---|
| api/workspace.py | Adds blocked-root guard inside _remote_terminal_workspace_candidate() before the _is_within early-return; fix is correct for the explicit registration/resolution paths but _profile_default_workspace() still returns blocked cwd values without validation. |
| tests/test_remote_terminal_workspace.py | Adds parametrized regression test covering both the exact blocked root (/etc) and a sub-path (/etc/ssh); tests both validate_workspace_to_add and resolve_trusted_workspace. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["validate_workspace_to_add(path)\nor resolve_trusted_workspace(path)"] --> B["_remote_terminal_workspace_candidate(path)"]
B --> C{"remote cwd\nconfigured?"}
C -- No --> D["return None"]
C -- Yes --> E["resolve candidate & base"]
E --> F{"NEW: _is_blocked_workspace_path\n(candidate) OR (base)?"}
F -- Yes --> G["return None ✅ (fix)"]
F -- No --> H{"candidate == base\nor _is_within(base)?"}
H -- No --> I["return None"]
H -- Yes --> J["return candidate"]
G --> K["Caller falls through to\n_is_blocked_workspace_path check"]
K --> L["ValueError: Path points to\na system directory ✅"]
M["get_last_workspace()"] --> N["valid_last_workspace(stored_path)"]
N --> B2["_remote_terminal_workspace_candidate(stored_path)"]
B2 --> O{"blocked cwd?"}
O -- Yes --> P["return None (after fix)"]
P --> Q["valid_last_workspace returns None"]
Q --> R["_profile_default_workspace()"]
R --> S["returns terminal.cwd resolved\n⚠️ no blocked-root check"]
S --> T["get_last_workspace returns /private/etc"]
T --> U["media handler: allowed_roots += /private/etc ⚠️"]
T --> V["resolve_trusted_workspace raises ValueError — recovery broken ⚠️"]
Comments Outside Diff (1)
-
api/workspace.py, line 103-144 (link)_profile_default_workspace()returns a blocked system root without validation_profile_default_workspace()returnsterminal.cwdresolved —/private/etcon macOS — without ever calling_is_blocked_workspace_path. This becomes a problem because this fix (correctly) makes_remote_terminal_workspace_candidate()returnNonefor every path under a blocked cwd, which causesget_last_workspace()to always fall back to_profile_default_workspace()for these profiles. That fallback value is then used in two places without a blocked-root guard:-
_resolve_chat_workspace_with_recovery(line 11391) callsresolve_trusted_workspace(get_last_workspace())as a recovery fallback. After this fix, that call raisesValueErrorfor a profile withcwd="/etc", breaking workspace recovery for any session on that profile — the recovery helper itself crashes. -
The media file serving handler (lines 9421-9424) adds
Path(get_last_workspace()).resolve()toallowed_rootsif it.is_dir(). With the fallback returning/private/etc, any request to that endpoint withpathunder/etcpasses thewithin_allowedcheck.
The minimal fix is to add a blocked-root check at the
terminal.cwdbranch of_profile_default_workspace(), or haveget_last_workspace()validate the return value of_profile_default_workspace()against the same guard. -
Reviews (2): Last reviewed commit: "test: cover remote blocked root subpaths" | Re-trigger Greptile
|
Addressed in I expanded the regression test to cover both the blocked root ( Local validation: |
…fix nesquena#3731) (nesquena#3744) * fix: reject blocked roots for remote workspaces * test: cover remote blocked root subpaths * docs(changelog): v0.51.296 security fix + backfill v0.51.295 entries - v0.51.296: nesquena#3731 remote-workspace blocked-root rejection. - Backfill the v0.51.295 release block (the nesquena#3739 model-pick entry + promote the nesquena#3570 revert out of [Unreleased]) which a stage-rebuild dropped from the prior release's CHANGELOG. git-describe versioning makes CHANGELOG-after-tag acceptable. --------- Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com> Co-authored-by: nesquena-hermes <[email protected]>
Summary
This hardens remote-terminal workspace resolution so the remote-workspace shortcut cannot accept blocked local system roots such as
/etcwhen the configured remote terminal cwd is also under that root.The normal local workspace path already rejects these roots, but
_remote_terminal_workspace_candidate()returned early for paths under the remote cwd before the blocked-root guard ran. Later workspace file operations uses.workspaceas a localPath, so accepting a remote cwd like/etccan convert the intended remote-side path into a trusted local workspace root.Security impact
Before this change, with an SSH/remote terminal profile whose target-side cwd is
/etc, workspace resolution accepted/etcas/private/etcon macOS. A session created with that workspace could then pass the workspace to local file helpers, allowing reads from local system files such ashoststhrough workspace file-read paths.Redacted local proof before the fix:
After the fix, both registration and trusted workspace resolution reject the same root:
Fix
_is_blocked_workspace_path()guard inside_remote_terminal_workspace_candidate()before accepting candidate paths under the remote cwd./etcas cwd.Validation
Duplicate check
I checked public issue/PR searches for remote terminal workspace/system-root wording and did not find an exact duplicate. The closest visible issue was #3673, but that appears to cover terminal backend behavior rather than this blocked-root workspace/file-boundary case.