Skip to content

fix(sandbox): protect daemon token file#685

Open
PierrunoYT wants to merge 2 commits into
Gitlawb:mainfrom
PierrunoYT:agent/protect-daemon-token-file
Open

fix(sandbox): protect daemon token file#685
PierrunoYT wants to merge 2 commits into
Gitlawb:mainfrom
PierrunoYT:agent/protect-daemon-token-file

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • remove ZERO_DAEMON_REMOTE_TOKEN_FILE from inherited sandbox command environments
  • add the existing token-file target to the normalized credential deny-read profile
  • retain existence filtering, symlink normalization, deduplication, and explicit AllowRead opt-outs
  • cover both environment scrubbing and runtime permission-profile construction with regression tests

Root cause

The sandbox scrubbed the inline ZERO_DAEMON_REMOTE_TOKEN value but left its file-pointer alternative in the child environment. Under the read-all workspace posture, a sandboxed command could read that pointer and then the bearer-token file outside the workspace.

The pointer is now scrubbed on every platform. Its target is protected by the existing credential deny-read mechanism on platforms where that mechanism is enabled. Windows filesystem deny-read remains subject to the existing ACL-model limitation tracked by #662.

Fixes #677

Validation

Known baseline/environment limitations

Summary by CodeRabbit

  • Security

    • Enhanced sandbox protections to deny read access to the configured daemon token file.
    • Sensitive daemon token file environment settings are now removed from sandbox environments.
    • Respecting read-access exceptions continues to work as expected.
  • Tests

    • Added coverage verifying daemon token files are protected and sensitive environment settings are scrubbed.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierrunoYT, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

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, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f3024c84-5fe8-481a-860b-bc1678c3c306

📥 Commits

Reviewing files that changed from the base of the PR and between 8533492 and 2248aca.

📒 Files selected for processing (2)
  • internal/sandbox/runner.go
  • internal/sandbox/runner_test.go

Walkthrough

The sandbox now protects the file referenced by ZERO_DAEMON_REMOTE_TOKEN_FILE by denying reads to it and removing the variable from sandboxed environments. Tests cover deny-read path construction, opt-outs, permission profiles, and environment scrubbing.

Changes

Daemon token protection

Layer / File(s) Summary
Credential deny-read integration
internal/sandbox/profile.go, internal/sandbox/manager_test.go
Credential deny-read paths include the trimmed daemon token file target, while tests cover normalization, AllowRead exclusions, missing home resolution, and permission profiles.
Sensitive environment scrubbing
internal/sandbox/runner.go, internal/sandbox/runner_test.go
ZERO_DAEMON_REMOTE_TOKEN_FILE is removed from sandbox environments and included in scrubbing test input.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • Gitlawb/zero#660 — Adds related daemon secret environment scrubbing.
  • Gitlawb/zero#681 — Modifies the same credential deny-read logic and tests.
  • Gitlawb/zero#682 — Updates the sandbox environment scrubbing pipeline for credential variables.

Suggested reviewers: kevincodex1, vasanthdev2004

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes remove ZERO_DAEMON_REMOTE_TOKEN_FILE from the sandbox env, add its target to DenyRead, and add both required regression tests.
Out of Scope Changes check ✅ Passed The modified files stay focused on the token-file leak fix and related tests, with no clear unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main sandbox change: protecting the daemon token file.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PierrunoYT PierrunoYT marked this pull request as ready for review July 14, 2026 21:05
Copilot AI review requested due to automatic review settings July 14, 2026 21:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR closes a sandbox escape where ZERO_DAEMON_REMOTE_TOKEN_FILE could be inherited by sandboxed commands (allowing them to locate and read the daemon bearer token file under the read-all posture). It scrubs the pointer env var across platforms and extends the existing “credential deny-read” profile logic to also deny reads of the referenced token file where deny-read enforcement is supported.

Changes:

  • Scrub ZERO_DAEMON_REMOTE_TOKEN_FILE from sandbox command environments (in addition to the inline token env var).
  • Extend credentialDenyReadPaths to include the path named by ZERO_DAEMON_REMOTE_TOKEN_FILE (alongside GOOGLE_APPLICATION_CREDENTIALS) and plumb this through the pure helper.
  • Add/extend regression tests covering env scrubbing and permission-profile deny-read construction (skipping the deny-read assertion on Windows per existing platform limitations).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
internal/sandbox/runner.go Adds ZERO_DAEMON_REMOTE_TOKEN_FILE to the sandbox env scrub list.
internal/sandbox/runner_test.go Extends env scrubbing regression test to ensure the pointer env var is removed.
internal/sandbox/profile.go Adds ZERO_DAEMON_REMOTE_TOKEN_FILE to default credential deny-read path construction and updates helper signature/docs.
internal/sandbox/manager_test.go Updates credential deny-read tests for the new parameter and adds a profile-level regression test for daemon token file denial (non-Windows).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Deny writes to the daemon token file on macOS as well
    internal/sandbox/profile.go:176
    The new target enters DenyRead, but the Seatbelt backend translates that only into file-read* and unlink denials. Its broad file-write* allowance still covers every workspace root and the default temporary roots. Therefore, when ZERO_DAEMON_REMOTE_TOKEN_FILE names a file under /tmp or another writable root, a sandboxed command can discover the filename from its parent directory and overwrite or truncate the bearer-token file. This makes the remote bridge unavailable and can replace its credential on a restart/reload. Add a write denial for credential DenyRead files in the Seatbelt profile (and a macOS regression case for a token under a writable temporary root).

Address code review on PR Gitlawb#685: the Seatbelt profile only translated
DenyRead entries into file-read* and file-write-unlink denials. The
broad file-write* allowance for workspace/temp write roots still
covered a DenyRead file (e.g. the file ZERO_DAEMON_REMOTE_TOKEN_FILE
names) if it happened to sit under one of them, so a sandboxed command
could discover and overwrite/truncate the daemon bearer-token file
even though it couldn't read or delete it.

A file a sandboxed command must not read has no legitimate reason to
be written either, so seatbeltProfileFromPermissionProfile now also
emits a full file-write* deny for every DenyRead path, placed after
the broad write allow (deny rules that follow an allow win, matching
the existing DenyWrite/metadata-carveout ordering).

Adds a regression test with a DenyRead file under a writable /tmp
root, and extends the existing deny-ordering test to assert the new
file-write* rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewing against commit 2248aca8 (head). The macOS Seatbelt fix (patch 2/2) is the right primitive: a DenyRead file that's also under a writable root was overwritable/truncatable because the prior profile only emitted file-read* and file-write-unlink, not file-write*. Denying the full write direction for every DenyRead path is correct, the ordering (deny after the broad allow) is correct, and TestSeatbeltProfileDeniesWritesToDenyReadUnderWritableRoot covers both the rule presence and the ordering. The TestSeatbeltProfileProtectsMetadataAndDenyOrdering extension covers the general case.

LGTM.

Cross-PR note: #685 depends on the credentialDenyReadPathsIn signature change from #681 (daemon token file as a parameter) and the scrubSensitiveEnv plumbed sensitiveEnvKeys from #682. Recommend rebasing #685 onto #681 + #682 in that order.

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Local review: built and ran go test ./internal/sandbox on darwin/arm64; all pass. The deny-write-for-DenyRead fix is a genuine security improvement (closes the truncate/overwrite bypass under a writable root). One integration note.

// credentialDenyReadPathsIn is the pure core of credentialDenyReadPaths,
// separated so tests can exercise it against a synthetic home directory.
func credentialDenyReadPathsIn(home string, googleCredentials string, allowRead []string) []string {
func credentialDenyReadPathsIn(home string, googleCredentials string, daemonTokenFile string, allowRead []string) []string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Sibling-PR merge conflict on credentialDenyReadPathsIn signature

This PR keeps the flat (home, googleCredentials, daemonTokenFile, allowRead) signature, while #681 rewrites the same function to take a credentialPathOptions struct. Both PRs also edit manager_test.go and runner_test.go against the same test. Each merges into main cleanly on its own, but only one lands as-is; the other must rebase. Also note #682 changes scrubSensitiveEnv in runner.go (this PR adds ZERO_DAEMON_REMOTE_TOKEN_FILE to the same function's literal list) -> sequence the three sandbox credential PRs.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Protect the configured symlink pathname as well as its target
    internal/sandbox/profile.go:200
    normalizeProfilePaths resolves ZERO_DAEMON_REMOTE_TOKEN_FILE through symlinks before it is added to DenyRead. If the configured pathname is a symlink under a writable root such as /tmp, the new deny rules protect only its current referent; a sandboxed command can unlink the writable symlink and recreate a regular file at the configured pathname. On the next remote-daemon start, TokenFromEnv reads that replacement pathname and accepts the attacker-chosen bearer token (or fails, causing a denial of service). Preserve and deny the lexical configured path in addition to its resolved target, and add a symlink-replacement regression test.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving clean security hardening. Scrubbing ZERO_DAEMON_REMOTE_TOKEN_FILE from child envs and adding its target to the credential deny-read set closes a real hole (a sandboxed command could otherwise resolve the pointer and read the daemon bearer-token file under the read-all posture), and extending the macOS seatbelt profile to file-write*-deny every DenyRead path is the right fix: denyReadRules only blocked read and unlink, leaving a credential file under a writable root overwritable/truncatable. I checked the Linux bubblewrap path and it already bind-mounts DenyRead targets read-only, so this just brings macOS to parity. One thing to be aware of: the write-deny now covers all DenyRead paths (~/.aws, ~/.azure, etc.), so no sandboxed command can update cloud creds consistent with the existing unlink-deny and fine under the current threat model, just calling it out.

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.

ZERO_DAEMON_REMOTE_TOKEN_FILE leaks the daemon bearer token into sandboxed commands

6 participants