Skip to content

fix(built-in-tools): resolve symlink dir containment before probing it#387

Merged
mbektas merged 1 commit into
plmbr:mainfrom
herikwebb:promote/security/fix-search-files-symlink-workspace-20260704140058
Jul 16, 2026
Merged

fix(built-in-tools): resolve symlink dir containment before probing it#387
mbektas merged 1 commit into
plmbr:mainfrom
herikwebb:promote/security/fix-search-files-symlink-workspace-20260704140058

Conversation

@herikwebb

Copy link
Copy Markdown
Contributor

_explicit_descendable stat'd an outbound symlink's target via
is_dir(follow_symlinks=True) before the workspace-containment check.
Resolve the path first, reject anything outside the workspace, and only
os.path.isdir() an in-workspace target -- so an outbound symlink is never
stat-followed past the boundary during enumeration. Behavior is unchanged
(verified: 13 sandbox tests pass, 1300+ fuzz trials match Path.glob).

@pjdoland pjdoland added the bug Something isn't working label Jul 9, 2026

@pjdoland pjdoland 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.

Thanks for this. The core change reads cleanly, and I really like that the safe walker enforces confinement during enumeration rather than only at the per-match gate. The docstrings on _glob_pattern_escapes and _explicit_descendable make the intent easy to follow.

One thing I would like to raise before approving. The fix hardens search_files, but the sibling list_files tool in the same file still enumerates through list_dir.glob(...) with only the root sandboxed via _get_safe_path. Its matches are never routed back through the workspace boundary, so the same enumeration leak this PR closes for search_files still applies there. For example, list_files(pattern="../*"), or a link -> /outside symlink with list_files(pattern="link/*"), will list [file] ../secret.txt and outside-directory contents, disclosing the names and existence of host paths outside the workspace (contents are not exposed, since list_files only lists).

I checked the rest of this stack (388 through 391) and none of them touch this file, so it does not look like list_files gets picked up later. Could we extend the same _iter_pattern_matches / boundary treatment to list_files in a revision? If you would rather keep this PR tightly scoped, splitting it into a fast follow-up works just as well. Happy to help however is easiest.

Everything else looks good to me. Thanks again!

@mbektas

mbektas commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

@herikwebb can you resolve the conflicts?

_explicit_descendable stat'd an outbound symlink's target via
is_dir(follow_symlinks=True) before the workspace-containment check.
Resolve the path first, reject anything outside the workspace, and only
os.path.isdir() an in-workspace target -- so an outbound symlink is never
stat-followed past the boundary during enumeration. Behavior is unchanged
(verified: 13 sandbox tests pass, 1300+ fuzz trials match Path.glob).
@herikwebb
herikwebb force-pushed the promote/security/fix-search-files-symlink-workspace-20260704140058 branch from b177901 to 12e657c Compare July 16, 2026 19:31
@herikwebb

Copy link
Copy Markdown
Contributor Author

Rebased onto main to resolve the conflict with #386 (which landed the initial search_files workspace-confinement fix in the meantime).

The overlap was in search_files: #386 kept Path.glob and added a per-match safe_jupyter_path gate in the read loop, whereas this PR replaces Path.glob with the sandbox-aware _iter_pattern_matches walker so outbound paths are never enumerated — and it already funnels every match through the same safe_jupyter_path gate during enumeration. #386's per-match re-resolution loop is therefore subsumed; the resolved code drops it and iterates the pre-gated (safe_file, rel_path) tuples the walker produces.

The test file was an add/add conflict; this PR's suite is a strict superset of #386's (both original tests retained, plus coverage for traversal rejection, existence non-leakage, non-descent of outbound symlink dirs, glob character classes, shallow/recursive depth, dot-segment normalization, path-aware file_pattern, in-workspace symlink dirs, cycle termination, and POSIX case sensitivity).

Verified locally: tests/test_builtin_search_files_sandbox.py 13/13, and 59/59 across the built-in-tools sandbox suites.

@mbektas mbektas 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.

LGTM, thanks!

@mbektas
mbektas merged commit 1084d60 into plmbr:main Jul 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants