fix(kvb-org-organize): no hook runs a binary planted on the inherited PATH
Hooks run outside the sandbox but inherit a PATH whose directories sandboxed agents of the same uid can write (a virtualenv's bin activated first by the shell profile), so a file planted there under the name of docker, git or notify-send ran unsandboxed. hookpath.py keeps only the PATH directories that, with every parent, belong to another uid and that neither group nor others can write, as real paths; inside the sandbox's user namespace root shows as the overflow uid and still counts as sealed. watch.py, radio.py and council.py set that PATH before they start anything, and the cleanup child runs with -I. Both reminder hooks pin PATH=/usr/bin:/bin, and the install registers them with /usr/bin/bash and moves older bare bash and python3 entries to the absolute paths. 🎯 Fix: 90 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 💰 Value: ? 🏆 Score: 86 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
94542e7b92
commit
454670a77e
13 files changed
+193
-5
No files matched your search
@@ -89,6 +89,28 @@ class TestCoachHour(unittest.TestCase):
|
||||
self.assertIn("kvb-coach", self._run("2026-11-03T15"))
|
||||
self.assertEqual(sorted(p.name for p in self.hours.iterdir()), ["2026-11-03T15"])
|
||||
|
||||
def test_a_binary_planted_on_the_inherited_path_never_runs(self):
|
||||
planted = Path(self.tmp.name) / "planted"
|
||||
planted.mkdir()
|
||||
marker = Path(self.tmp.name) / "ran"
|
||||
for name in ("mkdir", "find", "cat", "date"):
|
||||
fake = planted / name
|
||||
fake.write_text(f"#!/bin/sh\n/usr/bin/touch '{marker}'\n", encoding="utf-8")
|
||||
fake.chmod(0o755)
|
||||
subprocess.run(
|
||||
["bash", str(HOOK)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={
|
||||
"PATH": f"{planted}:/usr/bin:/bin",
|
||||
"HOME": self.tmp.name,
|
||||
"COACH_STAMP_DIR": str(self.hours),
|
||||
"COACH_HOUR": "2026-11-03T14",
|
||||
},
|
||||
)
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -97,6 +97,29 @@ class TestDreamHook(unittest.TestCase):
|
||||
self.assertEqual(self._run(payload), "")
|
||||
self.assertFalse(self.stamps.exists())
|
||||
|
||||
def test_a_binary_planted_on_the_inherited_path_never_runs(self):
|
||||
planted = Path(self.tmp.name) / "planted"
|
||||
planted.mkdir()
|
||||
marker = Path(self.tmp.name) / "ran"
|
||||
for name in ("python3", "date", "mkdir", "cat"):
|
||||
fake = planted / name
|
||||
fake.write_text(f"#!/bin/sh\n/usr/bin/touch '{marker}'\n", encoding="utf-8")
|
||||
fake.chmod(0o755)
|
||||
subprocess.run(
|
||||
["bash", str(HOOK)],
|
||||
input=self._session("a1"),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
env={
|
||||
"PATH": f"{planted}:/usr/bin:/bin",
|
||||
"HOME": self.tmp.name,
|
||||
"DREAM_STAMP_DIR": str(self.stamps),
|
||||
"DREAM_INTERVAL_SECONDS": "43200",
|
||||
},
|
||||
)
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
|
||||
class TestDreamNotification(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -233,6 +256,19 @@ class TestSettingsRegistration(unittest.TestCase):
|
||||
self.assertNotRegex(command, r"(^|&& )python3 ")
|
||||
self.assertEqual(len(self._hooks(data, "PreToolUse", "kvb-org-queue/scripts/queue.py")), 1)
|
||||
|
||||
def test_reminders_run_the_system_bash_and_old_entries_move_to_it(self):
|
||||
old = f'bash "{self.tmp.name}/skills/kvb-dream/hooks/reminder.sh"'
|
||||
self.settings.write_text(
|
||||
json.dumps({"hooks": {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": old}]}]}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
data = self._patch()
|
||||
reminders = [hook["command"] for hook in self._hooks(data, "UserPromptSubmit", "/hooks/reminder.sh")]
|
||||
self.assertEqual(len(reminders), 2)
|
||||
for command in reminders:
|
||||
with self.subTest(command=command):
|
||||
self.assertTrue(command.startswith("/usr/bin/bash "))
|
||||
|
||||
def test_council_config_deny_is_added_once_next_to_existing_rules(self):
|
||||
self.settings.write_text(json.dumps({"permissions": {"deny": ["Bash(rm -rf:*)"]}}), encoding="utf-8")
|
||||
self._patch()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
HOOKPATH = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "hookpath.py"
|
||||
|
||||
|
||||
def _load():
|
||||
spec = importlib.util.spec_from_file_location("hookpath_under_test", HOOKPATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@unittest.skipIf(os.getuid() == 0, "every directory root creates is root-owned")
|
||||
class TestHookpath(unittest.TestCase):
|
||||
def test_only_sealed_directories_survive_as_real_paths(self):
|
||||
hookpath = _load()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = Path(tmp)
|
||||
own = base / "bin"
|
||||
own.mkdir()
|
||||
link = base / "system"
|
||||
link.symlink_to("/usr/bin")
|
||||
path = os.pathsep.join([str(own), str(link), "", "/tmp", str(base / "missing"), "/usr/bin", "/bin"])
|
||||
expected = list(dict.fromkeys(str(Path(d).resolve()) for d in ("/usr/bin", "/bin")))
|
||||
self.assertEqual(hookpath.trusted(path).split(os.pathsep), expected)
|
||||
self.assertEqual(hookpath.trusted(str(own)), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -416,6 +416,24 @@ class TestRadio(unittest.TestCase):
|
||||
self.assertEqual(self._tool("s2"), ["MAYDAY One: fire"])
|
||||
self.assertEqual(os.listdir(victims), [])
|
||||
|
||||
def test_the_operator_notice_runs_no_binary_planted_on_the_path(self):
|
||||
planted = self.root.parent / "planted"
|
||||
planted.mkdir()
|
||||
marker = self.root.parent / "ran"
|
||||
fake = planted / "council-probe"
|
||||
fake.write_text(f"#!/bin/sh\n/usr/bin/touch '{marker}'\n", encoding="utf-8")
|
||||
fake.chmod(0o755)
|
||||
self.config.write_text(json.dumps({"notify": ["council-probe"]}), encoding="utf-8")
|
||||
self._send("s1", "send", "mayday", "fire")
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(RADIO), "tool"],
|
||||
input=json.dumps({"session_id": "s2"}), capture_output=True, text=True,
|
||||
env={**self._env(None), "PATH": str(planted)}, check=False, timeout=30,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("MAYDAY", result.stdout)
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
def test_the_hooks_skip_a_session_lock_another_process_holds(self):
|
||||
self._send("s1", "post", "board")
|
||||
lock = self.root / "radio-seen" / "s2.lock"
|
||||
|
||||
@@ -406,6 +406,32 @@ class TestWatchCommands(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_the_watch_hook_runs_no_binary_planted_on_the_path(self):
|
||||
planted = self.base / "planted"
|
||||
planted.mkdir()
|
||||
marker = self.base / "ran"
|
||||
for name in ("docker", "council-probe"):
|
||||
fake = planted / name
|
||||
fake.write_text(f"#!/bin/sh\n/usr/bin/touch '{marker}'\n", encoding="utf-8")
|
||||
fake.chmod(0o755)
|
||||
self.cfg["notify"] = ["council-probe"]
|
||||
self.cfg["thresholds_gb"] = {"warn": 1e9, "stop": 1e9, "critical": 1e9}
|
||||
self._write_config()
|
||||
env = {
|
||||
"PATH": str(planted),
|
||||
"HOME": str(self.base),
|
||||
"AGENTS_COUNCIL_ROOT": str(self.root),
|
||||
"AGENTS_COUNCIL_CONFIG": str(self.config),
|
||||
}
|
||||
payload = {"session_id": "s1", "hook_event_name": "UserPromptSubmit"}
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(WATCH), "watch"],
|
||||
capture_output=True, text=True, env=env, input=json.dumps(payload), check=False, timeout=60,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("RESOURCE CRITICAL", result.stdout)
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
def test_the_watch_hook_skips_an_alert_line_that_is_no_alert(self):
|
||||
self.root.mkdir()
|
||||
(self.root / "alerts.jsonl").write_text(
|
||||
|
||||
Reference in new issue
Block a user