Skip to content

fix: sandbox path jail bypassed by a slashless symlink (escape)#249

Open
Mubashirrrr wants to merge 2 commits into
rohitg00:mainfrom
Mubashirrrr:fix/sandbox-symlink-jail-bypass
Open

fix: sandbox path jail bypassed by a slashless symlink (escape)#249
Mubashirrrr wants to merge 2 commits into
rohitg00:mainfrom
Mubashirrrr:fix/sandbox-symlink-jail-bypass

Conversation

@Mubashirrrr
Copy link
Copy Markdown

What

The sandbox in 19-capstone-projects/26-sandbox-runner-denylist can be escaped with a slashless symlink, reading files outside the project root — the exact thing its "symlink-safe path jail" is documented to prevent.

Why

_check_path_jail only resolves/prefix-checks an argument when _looks_like_path(arg) is true, and that returns true only when the arg contains //\ or is exactly ./..:

for arg in argv[1:]:
    if not _looks_like_path(arg):
        continue          # <-- slashless names are never jail-checked
    ...
    resolved = os.path.realpath(candidate)
    if resolved != root and not resolved.startswith(root + os.sep):
        return "...outside project root..."

A symlink whose name has no separator — e.g. link.txt in the project root pointing at /etc/passwd — is skipped, so cat link.txt runs and exfiltrates host files. The tell: sub/link.txt (same symlink one dir deep, has a slash) is correctly denied. The command runs as a real subprocess with cwd=root, so the jail is the only thing standing between a sandboxed tool and arbitrary host reads.

Fix

Also jail-check arguments that exist on disk under the root, so a slashless symlink is resolved and refused (os.path.lexists catches the symlink even if its target is missing). Non-path tokens that don't exist under root are still skipped, and real files under root still resolve inside root (no false denials).

Test

$ python3 -m pytest tests/ -q
26 passed

New test_slashless_symlink_to_outside_is_denied fails on the previous code (jail returned None/allowed) and passes after the fix.

🤖 Generated with Claude Code

26-sandbox-runner-denylist: the path jail (_check_path_jail) only resolves and
prefix-checks arguments for which _looks_like_path() is true, which requires a
path separator (or exactly ./..). A symlink whose name has no slash — e.g.
`link.txt` in the project root pointing at /etc/passwd — is therefore never
jail-checked, so `cat link.txt` escapes the jail and reads files outside the
project root, exactly what the module's advertised "symlink-safe path jail" is
meant to prevent. (`sub/link.txt`, having a slash, is correctly denied; the
asymmetry is the tell.)

Also jail-check arguments that exist on disk under the root (os.path.lexists),
so a slashless symlink is resolved and refused. lexists catches the symlink
even when its target is missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6c5aad3-663b-4f6f-8790-2b1b09dc5be8

📥 Commits

Reviewing files that changed from the base of the PR and between 55d6eae and 1ed425b.

📒 Files selected for processing (1)
  • phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py

📝 Walkthrough

Walkthrough

Updates _check_path_jail to skip empty and flag-like args and to lexists-check slashless names under project_root, resolving them to ensure they don't escape the jail. Adds a regression test that denies a slashless symlink pointing outside the project root.

Changes

Path Jail Enhancement for Symlink Detection

Layer / File(s) Summary
Slashless Symlink Detection and Regression Test
phases/19-capstone-projects/26-sandbox-runner-denylist/code/main.py, phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py
_check_path_jail now continues past empty args and argv entries starting with -, and additionally uses os.path.lexists(os.path.join(root, arg)) to detect filesystem entries (including slashless symlinks) under project_root and verify their realpath remains inside the jail. A new test asserts a slashless symlink to an outside file is denied with an "outside project root" reason.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main security fix: a sandbox path jail vulnerability bypassed by slashless symlinks, which is the primary change across both modified files.
Description check ✅ Passed The description comprehensively explains the vulnerability, its root cause, the fix applied, and test verification, all directly related to the changeset modifications.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py (1)

119-131: 💤 Low value

Consider skipping the test on platforms without symlink support.

os.symlink() raises OSError on Windows without Developer Mode or admin privileges. If CI ever runs on Windows, this test would fail unexpectedly.

♻️ Optional: Add platform guard
     def test_slashless_symlink_to_outside_is_denied(self) -> None:
         # A symlink whose name has no slash points outside the root. The module
         # advertises a "symlink-safe path jail via realpath prefix check", so the
         # jail must resolve and refuse it even though _looks_like_path misses it.
         root, cfg = _make_root()
         outside_dir = tempfile.mkdtemp(prefix="sandbox-outside-")
         secret = os.path.join(outside_dir, "secret.txt")
         with open(secret, "w", encoding="utf-8") as fh:
             fh.write("TOP-SECRET\n")
+        try:
+            os.symlink(secret, os.path.join(root, "link.txt"))
+        except OSError:
+            self.skipTest("symlinks not supported on this platform")
-        os.symlink(secret, os.path.join(root, "link.txt"))
         reason = _check_path_jail(["cat", "link.txt"], cfg)
         self.assertIsNotNone(reason)
         self.assertIn("outside project root", reason)
🤖 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
`@phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py`
around lines 119 - 131, The test test_slashless_symlink_to_outside_is_denied
currently calls os.symlink unguarded; modify the test to skip when the
platform/user cannot create symlinks by first checking hasattr(os, "symlink")
and then attempting to create and immediately remove a small temporary symlink
inside the test (catching OSError); if creation fails, call
self.skipTest("symlink not supported or requires elevated privileges") before
calling _make_root/_check_path_jail so the test safely skips on Windows/other
envs without symlink support.
🤖 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.

Nitpick comments:
In
`@phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py`:
- Around line 119-131: The test test_slashless_symlink_to_outside_is_denied
currently calls os.symlink unguarded; modify the test to skip when the
platform/user cannot create symlinks by first checking hasattr(os, "symlink")
and then attempting to create and immediately remove a small temporary symlink
inside the test (catching OSError); if creation fails, call
self.skipTest("symlink not supported or requires elevated privileges") before
calling _make_root/_check_path_jail so the test safely skips on Windows/other
envs without symlink support.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5dc9e73c-1f29-4cc9-8162-75c6bc2c1042

📥 Commits

Reviewing files that changed from the base of the PR and between 44b9b14 and 55d6eae.

📒 Files selected for processing (2)
  • phases/19-capstone-projects/26-sandbox-runner-denylist/code/main.py
  • phases/19-capstone-projects/26-sandbox-runner-denylist/code/tests/test_main.py

os.symlink raises OSError on Windows without Developer Mode/admin, which
would fail this test spuriously in such CI. Skip instead of failing.

Addresses CodeRabbit review feedback on the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Mubashirrrr
Copy link
Copy Markdown
Author

Thanks — addressed in 1ed425b. The slashless-symlink test now skips instead of failing when the platform can't create symlinks (e.g. Windows without Developer Mode/admin). Verified the full suite (26 tests) still passes on a symlink-capable platform.

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