mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-08 16:28:25 +02:00
fix: validate attached redirection paths
This commit is contained in:
@@ -11,6 +11,7 @@ _GLOB_META = set('*?[')
|
|||||||
_WINDOWS_DRIVE_RE = re.compile(r'^[A-Za-z]:[\\/]')
|
_WINDOWS_DRIVE_RE = re.compile(r'^[A-Za-z]:[\\/]')
|
||||||
_WINDOWS_UNC_RE = re.compile(r'^(?:\\\\|//)[^\\/]+[\\/][^\\/]+')
|
_WINDOWS_UNC_RE = re.compile(r'^(?:\\\\|//)[^\\/]+[\\/][^\\/]+')
|
||||||
_ENV_ASSIGNMENT_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*=')
|
_ENV_ASSIGNMENT_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*=')
|
||||||
|
_REDIRECTION_TARGET_RE = re.compile(r'^(?:\d*)?(?:<>|>>?|<)(.+)$|^&>>?(.+)$')
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -118,6 +119,7 @@ def extract_path_candidates(payload: str) -> tuple[str, ...]:
|
|||||||
for token in (*tokens, *raw_tokens):
|
for token in (*tokens, *raw_tokens):
|
||||||
if not token or token.startswith('-') or _ENV_ASSIGNMENT_RE.match(token):
|
if not token or token.startswith('-') or _ENV_ASSIGNMENT_RE.match(token):
|
||||||
continue
|
continue
|
||||||
|
token = _strip_redirection_operator(token)
|
||||||
expanded = os.path.expandvars(os.path.expanduser(token))
|
expanded = os.path.expandvars(os.path.expanduser(token))
|
||||||
if _looks_like_path(token) or _looks_like_path(expanded):
|
if _looks_like_path(token) or _looks_like_path(expanded):
|
||||||
candidate = expanded if _looks_like_path(expanded) else token
|
candidate = expanded if _looks_like_path(expanded) else token
|
||||||
@@ -138,6 +140,13 @@ def _looks_like_path(token: str) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_redirection_operator(token: str) -> str:
|
||||||
|
match = _REDIRECTION_TARGET_RE.match(token)
|
||||||
|
if match is None:
|
||||||
|
return token
|
||||||
|
return next(group for group in match.groups() if group is not None)
|
||||||
|
|
||||||
|
|
||||||
def _is_windows_absolute(value: str) -> bool:
|
def _is_windows_absolute(value: str) -> bool:
|
||||||
return bool(_WINDOWS_DRIVE_RE.match(value) or _WINDOWS_UNC_RE.match(value))
|
return bool(_WINDOWS_DRIVE_RE.match(value) or _WINDOWS_UNC_RE.match(value))
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ class WorkspacePathScopeTests(unittest.TestCase):
|
|||||||
self.assertFalse(decision.allowed)
|
self.assertFalse(decision.allowed)
|
||||||
self.assertIn(str(outside.resolve()), decision.resolved or '')
|
self.assertIn(str(outside.resolve()), decision.resolved or '')
|
||||||
|
|
||||||
|
def test_attached_shell_redirection_targets_are_validated(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
workspace = root / 'workspace'
|
||||||
|
outside = root / 'outside'
|
||||||
|
workspace.mkdir()
|
||||||
|
outside.mkdir()
|
||||||
|
(outside / 'secret.txt').write_text('secret')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
('../outside/secret.txt', '../outside/error.log'),
|
||||||
|
extract_path_candidates(
|
||||||
|
'cat <../outside/secret.txt 2>../outside/error.log'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
decision = WorkspacePathScope.from_root(workspace).validate_payload(
|
||||||
|
'cat <../outside/secret.txt 2>../outside/error.log'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(decision.allowed)
|
||||||
|
self.assertIn(str(outside.resolve()), decision.resolved or '')
|
||||||
|
|
||||||
def test_explicit_worktree_roots_are_allowed(self) -> None:
|
def test_explicit_worktree_roots_are_allowed(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
|
|||||||
Reference in New Issue
Block a user