Skip to content

fix(sandbox): keep agent workspace off gateway state files - #79

Merged
adriannoes merged 4 commits into
developmentfrom
cursor/high-severity-issues-82df
Sep 22, 2026
Merged

adriannoes merged 4 commits into
developmentfrom
cursor/high-severity-issues-82df

Conversation

@cursor

@cursor cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

Summary

Default workspace_path was ~/.shellclaw, the same directory as gateway pairing tokens, memory.db, and config.toml. With default workspace_only=1, inbound Discord / WebChat / cron file tools treated those files as in-bounds.

Bug

Root cause

workspace_only is a prefix check against workspace_path. The default path was the process state dir, so the security control that is on by default authorized tool access to pairing tokens.

Fix

  • Default workspace_path to ~/.shellclaw/workspace and create that directory at tool init.
  • Deny auth_tokens.json, shellclaw.pid, shellclaw.log, and .shellclaw/config.toml / .shellclaw/memory.db even if they sit inside a custom workspace.
  • Block those token names in the shell allowlist and unsandboxed fallback list.

Validation

  • CI=true GATEWAY=1 make test (full suite, including new file/allowlist/config/shell cases)
Open in Web聽View Automation聽

Default workspace_path was ~/.shellclaw, the same tree as pairing
tokens, memory.db, and config.toml. With workspace_only on, inbound
Discord/webchat/cron file tools could read or overwrite those files.
Point the default at ~/.shellclaw/workspace and deny runtime state
paths even when an operator keeps the old workspace root.

Co-authored-by: esadrianno <esadrianno@gmail.com>
Keep the default workspace off ~/.shellclaw and the runtime-state denylist together with development's atomic writes, symlink rejection, and Jetson allowlist entries.
@adriannoes
adriannoes marked this pull request as ready for review September 22, 2026 16:50
@adriannoes
adriannoes self-requested a review as a code owner September 22, 2026 16:50
GCC -Werror=format-truncation rejected snprintf of a 256-byte directory plus /config.toml into another 256-byte buffer.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR moves the default tool workspace from ~/.shellclaw to ~/.shellclaw/workspace, and it refuses pairing tokens, the pid file, the log, and .shellclaw/config.toml / .shellclaw/memory.db when a custom workspace still contains them. The default split is the right fix, and the file-tool test checks that token bytes are not returned.

The compatibility denylist is incomplete. I compiled allowlist.c against a throwaway driver: absolute config.toml is blocked, and the three cases below are allowed (allowlist_check_shell_command returns 0).

Must Fix

  1. memory.db-wal and memory.db-shm are not reserved. src/core/memory.c sets PRAGMA journal_mode=WAL, so live memory and session pages sit in those sidecars. allowlist_path_is_runtime_state_file matches only the exact basename memory.db. With the workspace pointed at a .shellclaw directory, read_file / write_file and cat <abs>/.shellclaw/memory.db-wal are allowed. Match memory.db and memory.db-* when the parent directory is .shellclaw.
  2. Sandboxed shell chdirs into workspace_path (setup_child_process in src/sandbox/sandbox.c) but the new check only inspects the raw token. has_path_chars ignores a token that does not start with /, ~, or ., and a bare config.toml has no slash, so the parent-directory test returns "not reserved". cat config.toml and cat memory.db are allowed, then the child reads the state files. Resolve the token against the workspace root before deciding. tests/test_allowlist.c only asserts absolute cat %s paths, so CI stays green.

Should Fix

  • ensure_workspace_directory treats any EEXIST as success. If ~/.shellclaw/workspace is a symlink (including one planted before this upgrade), later realpath uses the link target as the workspace root and the default split is gone. lstat the final component and refuse a symlink or a non-directory.
  • The runtime-state loop sits under the workspace_only early return in allowlist_check_shell_command, and the unsandboxed fallback in src/tools/shell.c never calls it. sandbox_enabled stays 0 unless TOML sets it. Unsandboxed cat ~/.shellclaw/config.toml still runs. A global substring on config.toml is too broad. Run the same path predicate on that fallback for real path tokens.
  • test_runtime_state_files_rejected_inside_workspace returns without failing when mkdir fails, so a setup error looks like a pass. test_shell_blocked_auth_tokens passes a NULL config, so it only hits the fallback substring.

Nice to Have

  • test_defaults only checks that the path ends in /workspace, not that the parent is .shellclaw.
  • ensure_workspace_directory creates only the immediate parent. The default ~/.shellclaw/workspace is fine. A deeper custom path logs a mkdir failure and tools_init still returns 0. File access then fail-closes because the workspace does not resolve.
  • Every basename auth_tokens.json is reserved, not only under .shellclaw. That also blocks an unrelated project file and any command that merely mentions the name. The header comment says this is intentional.

Positive Highlights

  • Separating the tool root from the state directory fixes the default case without a new sandbox mechanism.
  • config.toml and memory.db are reserved only when the parent is .shellclaw, and test_allow_project_config_toml locks the other case.
  • The file-tool regression checks that secret-pair is absent from the tool result, not only that the call returned -1.
  • Example config, CHANGELOG, and the gateway oversize-file helper were updated for the new directory.
Open in Web聽View Automation聽

Sent by Cursor Automation: Adrianno鈥檚 personal code review

Comment thread src/sandbox/allowlist.c Outdated
Comment thread src/sandbox/allowlist.c
Comment thread src/core/bootstrap.c
WAL sidecars next to memory.db were readable, and a bare cat config.toml ran after the sandbox chdir. Also refuse a symlink workspace and apply the same state-file check on the unsandboxed shell path.
@adriannoes

Copy link
Copy Markdown
Collaborator

Should Fix items that were not inline threads are also in e63a7ec:

  • Unsandboxed shell runs allowlist_check_shell_command with a null config, so path tokens hit the runtime-state predicate without a global config.toml substring.
  • test_runtime_state_files_rejected_inside_workspace fails the test if mkdir fails.
  • test_defaults now requires the path to end in /.shellclaw/workspace.

Left as-is: a custom workspace whose parents do not exist still fail-closes when realpath misses (no recursive mkdir). Every basename auth_tokens.json stays reserved, as the header already documents.

Comment thread src/sandbox/allowlist.c
snprintf(expanded, sizeof(expanded), "%s", tok);
check = expanded;
}
if (allowlist_path_is_runtime_state_file(check)) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃敀 Agentic Security Review
Severity: HIGH

The new runtime-state denylist inspects raw strtok tokens and exact basenames. A quoted or globbed path such as cat "~/.shellclaw/auth_tok*" never matches auth_tokens.json / config.toml, has_path_chars skips the token because it does not start with /, ~, or ., and the unsandboxed fallback still calls this check with cfg == NULL.

Impact: An inbound Discord/WebChat/cron prompt that causes shell can still dump pairing tokens or config.toml after this PR鈥檚 default workspace split.

Fix in Cursor聽Fix in Web

Reviewed by Cursor Security Reviewer for commit e63a7ec. Configure here.

@adriannoes
adriannoes merged commit c8cd91c into development Sep 22, 2026
2 checks passed
@adriannoes
adriannoes deleted the cursor/high-severity-issues-82df branch September 22, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants