Skip to content

fix(design): refuse staging symlinks escaping into challenge secrets - #107

Open
echobt wants to merge 2 commits into
mainfrom
fix/design-symlink-collect
Open

fix(design): refuse staging symlinks escaping into challenge secrets#107
echobt wants to merge 2 commits into
mainfrom
fix/design-symlink-collect

Conversation

@echobt

@echobt echobt commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Hardens design sandbox / staging collectors so miner-planted symlinks under out/pages (and related staging paths) are not followed into the design-challenge mount NS (/run/base/*, /proc/1/environ).
  • Screenshot staging reads and agentic review workdir reads use the same no-follow gates; run_command denylist also covers /run/base and secret path needles.
  • Documents the finding as threat-model R15 (High); no key rotation in this PR.

Test plan

  • cargo fmt / clippy -D warnings on design-sandbox, design-challenge, challenge-agentic
  • Unit tests: collect_out / read_staged_text / .miner_env.json reject symlinks; screenshot read_staging_bytes refuses symlinks; agentic run_command forbids /run/base/... needles
  • cargo test -p design-sandbox -p challenge-agentic -p design-challenge --lib --tests
  • CI green on PR
  • Staging pin update / promote after merge (digest-only)

Summary by CodeRabbit

  • Security Enhancements

    • Hardened artifact, screenshot, and staging-file handling to reject symlinks and non-regular files.
    • Expanded protection against access to sensitive mounted paths during command execution.
    • Unsafe sandbox output is now rejected and reported as a terminal failure.
  • Documentation

    • Updated operator security guidance and threat-model documentation for symlink and sensitive-path protections.

Miner-controlled out/pages (and other staging paths) could be replaced with
symlinks into the design-challenge mount NS. Collect/read with symlink_metadata
+ O_NOFOLLOW, harden review run_command denylist, and document as R15.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change hardens sandbox and screenshot staging reads against symlinks and non-regular files. It adds Linux O_NOFOLLOW protection, blocks /run/base, classifies unsafe output as a miner failure, and updates tests and security documentation.

Changes

Sandbox security hardening

Layer / File(s) Summary
Hardened sandbox output reads
crates/design-sandbox/src/lib.rs, docs/DESIGN_CHALLENGE.md
Sandbox readers reject symlinks, non-regular files, and invalid UTF-8. Page and miner environment collection uses the readers. Tests cover unsafe staging files.
Secret-path command controls
crates/challenge-agentic/src/tools.rs
run_command rejects paths under /run/base. Tests cover challenge and API-key paths.
Screenshot validation and unsafe-output classification
crates/design-challenge/src/screenshot.rs, crates/design-challenge/src/orchestrator.rs
Screenshot reads use the protected reader and require regular output files. SandboxError::UnsafeOutput becomes a terminal miner failure.
Security model and operator guidance
docs/OPERATOR_SECURITY.md, docs/THREAT_MODEL.md
The operator checklist and threat model describe staging-symlink defenses and /run/base restrictions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Miner
  participant Sandbox
  participant StagingReader
  participant Orchestrator
  Miner->>Sandbox: produce staged pages and environment files
  Sandbox->>StagingReader: read staged outputs
  StagingReader-->>Sandbox: validated contents or UnsafeOutput
  Sandbox-->>Orchestrator: sandbox result
  Orchestrator-->>Miner: terminal miner failure for unsafe output
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preventing staging symlinks from exposing challenge secrets.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/design-symlink-collect

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/design-sandbox/src/lib.rs (1)

707-735: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not use simulation markers as proof that agent.py executed.

This test accepts sim-install-ok, sim-run-ok, and sim-stub as success. Those values do not prove that agent.py executed.

If this is only a simulation smoke test, rename it. Add an end-to-end challenge verification that forces DESIGN_FORCE_SIM=false and, when an OpenRouter key is configured, verifies intake, failure probes, leaf emission, raw weight submission, sealing, and sealed: true.

As per coding guidelines, do not treat sim-install-ok or sim-run-ok as proof that agent.py executed, and challenge verification must simulate end-to-end submission.

🤖 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 `@crates/design-sandbox/src/lib.rs` around lines 707 - 735, Rename
sim_runs_baseline_agent_py to clearly identify it as a simulation smoke test and
stop using sim-install, sim-run, or sim-stub markers as evidence that agent.py
executed. Add a separate end-to-end challenge verification that forces
DESIGN_FORCE_SIM=false and, when an OpenRouter key is configured, validates
intake, failure probes, leaf emission, raw weight submission, sealing, and a
sealed: true result through simulated end-to-end submission.

Source: Coding guidelines

🤖 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 `@crates/challenge-agentic/src/tools.rs`:
- Around line 475-478: Replace the lexical filtering around
run_command_forbidden_path with isolation that prevents commands from accessing
secret-bearing mounts, preferably removing run_command from those containers or
executing it in an isolated container or mount namespace. Preserve denial of
/run/base secret paths, and add coverage for glob-, variable-, and
interpreter-composed paths, including /proc/*/environ access, rather than
relying on shell-text inspection.

---

Outside diff comments:
In `@crates/design-sandbox/src/lib.rs`:
- Around line 707-735: Rename sim_runs_baseline_agent_py to clearly identify it
as a simulation smoke test and stop using sim-install, sim-run, or sim-stub
markers as evidence that agent.py executed. Add a separate end-to-end challenge
verification that forces DESIGN_FORCE_SIM=false and, when an OpenRouter key is
configured, validates intake, failure probes, leaf emission, raw weight
submission, sealing, and a sealed: true result through simulated end-to-end
submission.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bde1562-54e5-4dc4-8742-d2bbc06722bf

📥 Commits

Reviewing files that changed from the base of the PR and between a6ad6e7 and fb8e447.

📒 Files selected for processing (7)
  • crates/challenge-agentic/src/tools.rs
  • crates/design-challenge/src/orchestrator.rs
  • crates/design-challenge/src/screenshot.rs
  • crates/design-sandbox/src/lib.rs
  • docs/DESIGN_CHALLENGE.md
  • docs/OPERATOR_SECURITY.md
  • docs/THREAT_MODEL.md

Comment thread crates/challenge-agentic/src/tools.rs Outdated
Comment on lines +475 to +478
// File-mounted secrets + parent environ must stay unread (defense-in-depth;
// primary staging collectors also refuse symlinks — see design-sandbox).
let c = cmd.to_ascii_lowercase().replace('\\', "/");
if c.contains("/proc") || c.contains("review-secrets") || c.contains("openrouter_api_key") {
if run_command_forbidden_path(&c) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Replace the lexical denylist with a boundary that prevents secret reads.

run_command_forbidden_path checks raw shell text before bash -lc expands it. cat /r??/base/* bypasses both the /run/base and challenge_sk needles. cat /pr??/1/environ similarly bypasses the /proc needle.

This permits secret disclosure from the review container. Remove run_command from secret-bearing containers, or execute it in an isolated container or mount namespace that does not contain secret mounts. Do not rely on command-text filtering. Add tests for glob, variable, and interpreter-composed paths.

As per coding guidelines, run_command must keep /run/base secret paths denied.

Also applies to: 542-556, 803-815

🤖 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 `@crates/challenge-agentic/src/tools.rs` around lines 475 - 478, Replace the
lexical filtering around run_command_forbidden_path with isolation that prevents
commands from accessing secret-bearing mounts, preferably removing run_command
from those containers or executing it in an isolated container or mount
namespace. Preserve denial of /run/base secret paths, and add coverage for
glob-, variable-, and interpreter-composed paths, including /proc/*/environ
access, rather than relying on shell-text inspection.

Source: Coding guidelines

Share read_staged_bytes with screenshot staging, keep agentic denylist
delta to a single /run/base needle, and preserve the R15 collect fix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/challenge-agentic/src/tools.rs (1)

306-306: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Apply no-follow and regular-file checks to agentic workdir reads.

resolve_rel canonicalizes first, so later fs::metadata, Path::is_dir, and fs::read_to_string re-check targets by-path. Symlinked workdir entries can escape workdir or expose /proc/1/environ and /run/base/*. Directory symlinks can create an unbounded grep_walk traversal, and readers currently accept non-regular files.

Use descriptor-based, no-follow filesystem access for read_file, grep, tool_stat, metrics, pages, report reads, read_py, and load_primary_sources, and reject symlinks/non-regular files after the binding is fixed.

Also applies to: 319-324, 347, 395-398, 400-404, 424, 517, 681-682

🤖 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 `@crates/challenge-agentic/src/tools.rs` at line 306, Replace path-based reads
and metadata checks in read_file, grep/grep_walk, tool_stat, metrics, pages,
report reads, read_py, and load_primary_sources with descriptor-based no-follow
access rooted in the agentic workdir. After resolving each binding, reject
symlinks and require regular files for file readers; allow only validated
directories for traversal and prevent symlinked directories from expanding
grep_walk. Apply the same validation to all referenced metadata, read_to_string,
and is_dir call sites.
🤖 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.

Outside diff comments:
In `@crates/challenge-agentic/src/tools.rs`:
- Line 306: Replace path-based reads and metadata checks in read_file,
grep/grep_walk, tool_stat, metrics, pages, report reads, read_py, and
load_primary_sources with descriptor-based no-follow access rooted in the
agentic workdir. After resolving each binding, reject symlinks and require
regular files for file readers; allow only validated directories for traversal
and prevent symlinked directories from expanding grep_walk. Apply the same
validation to all referenced metadata, read_to_string, and is_dir call sites.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d92224d-8c68-4311-99f8-da5c390790e7

📥 Commits

Reviewing files that changed from the base of the PR and between fb8e447 and a77836d.

📒 Files selected for processing (4)
  • crates/challenge-agentic/src/tools.rs
  • crates/design-challenge/src/orchestrator.rs
  • crates/design-challenge/src/screenshot.rs
  • crates/design-sandbox/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/design-challenge/src/orchestrator.rs
  • crates/design-challenge/src/screenshot.rs
  • crates/design-sandbox/src/lib.rs

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.

1 participant