mirror of
https://github.com/instructkr/claw-code.git
synced 2026-10-17 00:00:05 +08:00
fix(security): deny unresolvable paths and enable Git bash detection on Windows
This commit is contained in:
1 parent
3463331171
commit
0d8b50b378
4 files changed
+88
-22
No files matched your search
+19
-2
@@ -72,7 +72,15 @@ class WorkspacePathScope:
|
||||
path = base / path
|
||||
expanded = self._expand_glob(path)
|
||||
for expanded_path in expanded:
|
||||
resolved = expanded_path.resolve(strict=False)
|
||||
try:
|
||||
resolved = expanded_path.resolve(strict=False)
|
||||
except (OSError, ValueError, RuntimeError):
|
||||
return PathScopeDecision(
|
||||
False,
|
||||
'path cannot be resolved or is invalid',
|
||||
str(candidate),
|
||||
str(expanded_path),
|
||||
)
|
||||
if not any(_is_relative_to(resolved, root) for root in self.roots):
|
||||
return PathScopeDecision(
|
||||
False,
|
||||
@@ -80,7 +88,16 @@ class WorkspacePathScope:
|
||||
str(candidate),
|
||||
str(resolved),
|
||||
)
|
||||
return PathScopeDecision(True, 'path is inside workspace scope', str(candidate), str(expanded[0].resolve(strict=False)))
|
||||
try:
|
||||
final_resolved = str(expanded[0].resolve(strict=False))
|
||||
except (OSError, ValueError, RuntimeError):
|
||||
return PathScopeDecision(
|
||||
False,
|
||||
'path cannot be resolved or is invalid',
|
||||
str(candidate),
|
||||
str(expanded[0]),
|
||||
)
|
||||
return PathScopeDecision(True, 'path is inside workspace scope', str(candidate), final_resolved)
|
||||
|
||||
def _expand_glob(self, path: Path) -> tuple[Path, ...]:
|
||||
path_text = str(path)
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
import os
|
||||
|
||||
def require_bash() -> bool:
|
||||
import shutil
|
||||
bash = shutil.which('bash')
|
||||
if os.name == 'nt':
|
||||
return False
|
||||
return bash is not None
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_bash_executable() -> str | None:
|
||||
if os.name == 'nt':
|
||||
for candidate in (
|
||||
r'C:\Program Files\Git\bin\bash.exe',
|
||||
r'C:\Program Files\Git\usr\bin\bash.exe',
|
||||
r'C:\Program Files (x86)\Git\bin\bash.exe',
|
||||
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe'),
|
||||
):
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
bash = shutil.which('bash')
|
||||
if bash and 'WindowsApps' not in bash:
|
||||
return bash
|
||||
return None
|
||||
|
||||
|
||||
def require_bash() -> bool:
|
||||
bash = get_bash_executable()
|
||||
if not bash:
|
||||
return False
|
||||
try:
|
||||
res = subprocess.run([bash, '-c', 'echo 1'], capture_output=True, text=True, timeout=2)
|
||||
return res.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
PRE_PUSH_HOOK = REPO_ROOT / '.github' / 'hooks' / 'pre-push'
|
||||
|
||||
@@ -24,11 +39,12 @@ PRE_PUSH_HOOK = REPO_ROOT / '.github' / 'hooks' / 'pre-push'
|
||||
class PrePushHookContractTests(unittest.TestCase):
|
||||
@unittest.skipUnless(require_bash(), 'Requires bash')
|
||||
def test_skip_escape_hatch_exits_successfully_with_stderr_notice(self) -> None:
|
||||
bash_cmd = get_bash_executable() or 'bash'
|
||||
env = os.environ.copy()
|
||||
env['SKIP_CLAW_PRE_PUSH_BUILD'] = '1'
|
||||
|
||||
result = subprocess.run(
|
||||
['bash', str(PRE_PUSH_HOOK)],
|
||||
[bash_cmd, str(PRE_PUSH_HOOK)],
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
check=True,
|
||||
|
||||
@@ -16,18 +16,38 @@ DOGFOOD_PROBE = REPO_ROOT / 'scripts' / 'dogfood-probe.py'
|
||||
|
||||
import sys
|
||||
|
||||
def require_bash() -> bool:
|
||||
def get_bash_executable() -> str | None:
|
||||
import os
|
||||
import shutil
|
||||
bash = shutil.which('bash')
|
||||
if os.name == 'nt':
|
||||
# On Windows, 'bash' often resolves to WSL which fails if not configured
|
||||
for candidate in (
|
||||
r'C:\Program Files\Git\bin\bash.exe',
|
||||
r'C:\Program Files\Git\usr\bin\bash.exe',
|
||||
r'C:\Program Files (x86)\Git\bin\bash.exe',
|
||||
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe'),
|
||||
):
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
bash = shutil.which('bash')
|
||||
if bash and 'WindowsApps' not in bash:
|
||||
return bash
|
||||
return None
|
||||
|
||||
|
||||
def require_bash() -> bool:
|
||||
bash = get_bash_executable()
|
||||
if not bash:
|
||||
return False
|
||||
return bash is not None
|
||||
try:
|
||||
res = subprocess.run([bash, '-c', 'echo 1'], capture_output=True, text=True, timeout=2)
|
||||
return res.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def run_next_id(roadmap: Path, script: Path = NEXT_ID) -> subprocess.CompletedProcess[str]:
|
||||
bash_cmd = get_bash_executable() or 'bash'
|
||||
return subprocess.run(
|
||||
['bash', str(script), str(roadmap)],
|
||||
[bash_cmd, str(script), str(roadmap)],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
@@ -166,6 +166,19 @@ class WorkspacePathScopeTests(unittest.TestCase):
|
||||
self.assertFalse(decision.allowed)
|
||||
self.assertIn('outside workspace scope', decision.reason)
|
||||
|
||||
def test_unresolvable_path_raises_oserror_is_denied(self) -> None:
|
||||
"""Verify that paths raising OSError during resolve() are explicitly denied."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
workspace = Path(tmp) / 'workspace'
|
||||
workspace.mkdir()
|
||||
scope = WorkspacePathScope.from_root(workspace)
|
||||
|
||||
from unittest.mock import patch
|
||||
with patch.object(Path, 'resolve', side_effect=OSError('dangling symlink or filesystem error')):
|
||||
decision = scope.validate_path(str(workspace / 'broken_link.txt'))
|
||||
self.assertFalse(decision.allowed)
|
||||
self.assertIn('cannot be resolved', decision.reason)
|
||||
|
||||
def test_glob_expansion_must_stay_inside_workspace(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
Reference in new issue
Block a user