-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix(sandbox): reject external symlink targets by default #4798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5729c6d
dc3cc58
9451dde
14c06a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,20 @@ def _validate_symlink_target( | |
| if not member.issym() or allow_external_symlink_targets: | ||
| return | ||
|
|
||
| # Member names already reject Windows drive and separator syntax. Targets are parsed | ||
| # as POSIX, so `C:/Windows` and `..\\..\\Windows` would otherwise read as ordinary | ||
| # relative components and pass the checks below. | ||
| if PureWindowsPath(member.linkname).drive: | ||
| raise UnsafeTarMemberError( | ||
| member=member.name, | ||
| reason=f"windows drive symlink target not allowed: {member.linkname}", | ||
| ) | ||
| if "\\" in member.linkname: | ||
| raise UnsafeTarMemberError( | ||
| member=member.name, | ||
| reason=f"windows path separator in symlink target: {member.linkname}", | ||
| ) | ||
|
|
||
| target = PurePosixPath(member.linkname) | ||
| if target.is_absolute(): | ||
| raise UnsafeTarMemberError( | ||
|
|
@@ -157,7 +171,11 @@ def strip_tar_member_prefix(data: io.IOBase, *, prefix: str | Path) -> io.IOBase | |
|
|
||
| out.seek(0) | ||
| with tarfile.open(fileobj=out, mode="r:*") as tar: | ||
| validate_tarfile(tar) | ||
| # Persisting a snapshot reads a workspace that already exists, so an external | ||
| # symlink here is something the workspace already contained rather than | ||
| # something an archive is introducing. Hydration is where that link would | ||
| # take effect, and every hydrate path rejects it. | ||
| validate_tarfile(tar, allow_external_symlink_targets=True) | ||
| out.seek(0) | ||
| return cast(io.IOBase, out) | ||
| except Exception: | ||
|
|
@@ -259,7 +277,7 @@ def validate_tarfile( | |
| skip_rel_paths: Iterable[str | Path] = (), | ||
| root_name: str | None = None, | ||
| allow_symlinks: bool = True, | ||
| allow_external_symlink_targets: bool = True, | ||
| allow_external_symlink_targets: bool = False, | ||
| ) -> None: | ||
| """Validate a workspace tar before handing it to a local or remote extractor. | ||
|
|
||
|
|
@@ -345,7 +363,7 @@ def validate_tar_bytes( | |
| reject_symlink_rel_paths: Iterable[str | Path] = (), | ||
| skip_rel_paths: Iterable[str | Path] = (), | ||
| root_name: str | None = None, | ||
| allow_external_symlink_targets: bool = True, | ||
| allow_external_symlink_targets: bool = False, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With this default flipped, Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, and correct. Reproduced it directly: My Windows run masked it. Those tests already fail at Fixed in dc3cc58. The venv round-trip and the three tests that seed an absolute link before replacing it now pass Full suite is back to the pristine baseline: 19 failures, identical set, identical causes (17 |
||
| ) -> None: | ||
| """Validate raw workspace tar bytes with the shared safe tar policy.""" | ||
|
|
||
|
|
@@ -369,7 +387,7 @@ def safe_extract_tarfile( | |
| tar: tarfile.TarFile, | ||
| *, | ||
| root: Path, | ||
| allow_external_symlink_targets: bool = True, | ||
| allow_external_symlink_targets: bool = False, | ||
| ) -> None: | ||
| """ | ||
| Safely extract a tar archive into `root`. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
allow_external_symlink_targetsnow defaults toFalse, callers can rely onvalidate_tarfile()/safe_extract_tarfile()to reject external symlink targets, but_validate_symlink_target()only treats POSIX absolute paths and POSIX..escapes as external. A tar containinglink -> C:/Windows/System32orlink -> ..\..\Windowsstill passes this default andsafe_extract_tarfile()will create that host-absolute/traversing link on Windows, so an untrusted archive can escape the extraction root despite the new strict default; reject Windows-drive and backslash target syntax before extraction. .agents/references/sandbox-runtime-boundary.mdL35-L39Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed and fixed in 9451dde. Under the strict default,
C:/Windows/System32,C:\Windows\System32,..\..\Windowsand\\server\sharewere all accepted, because the target is parsed as aPurePosixPathwhile_raise_if_windows_member_pathonly covers member names._validate_symlink_targetnow rejects a Windows drive or a backslash in the target, mirroring that helper, with a parametrized test over those four shapes.One correction on the impact.
safe_extract_tarfilehas a single caller,unix_local.py:1118, andunix_local.pyraisesImportErroron win32 at module import, so it cannot run on a Windows host. Every other caller validates before handing the archive to a remote or containerised extractor, per thevalidate_tarfiledocstring. So this was an incomplete guarantee in strict mode rather than a reachable escape.