From 88d579af0887119c06ddb3fb509d348e9c8a4024 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Sun, 30 Aug 2026 22:51:17 +0700 Subject: [PATCH] =?UTF-8?q?fix(security):=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D0=B0=20=D1=81=20=D1=82=D0=B8=D0=BB=D1=8C=D0=B4=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D1=83=20=D0=BA=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B0=20=D1=83=D1=87=D1=91=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка A37 исполнением. Защита границы рабочей области работает, обходы через ../ и ~ в validate_path отсекаются, каталоги учётных данных закрыты. Но в validate_command нашлась дыра ровно в том месте, ради которого guard и делался. Тильда и переменные окружения не раскрывались перед проверкой. Путь "~/.hermes/agy_profiles" не считался абсолютным, склеивался с каталогом проекта в путь с буквальным "~" внутри и признавался допустимым. Измерено: rm -rf ~/.hermes/agy_profiles РАЗРЕШЕНО rm -rf ~/.ssh РАЗРЕШЕНО rm -rf $HOME/.hermes РАЗРЕШЕНО тот же путь абсолютным отказ тот же путь через validate_path отказ То есть самый естественный способ написать опасную команду обходил защиту, а абсолютный путь — нет. После правки все три отклоняются, штатное удаление внутри проекта по-прежнему разрешено. Добавлен тест, удерживающий это свойство. Проверено отдельно, что защита не ломает продукт: страница, app.js, health и snapshot отдают 200, в снапшоте 13 ролей, секретов в ответе нет, проверка обновлений работает. 508 passed, ruff чисто. Co-Authored-By: Claude Opus 5 --- .../router/security_guard.py | 12 +++++++- tests/test_a37_isolation_guards.py | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/antigravity_provider/router/security_guard.py b/src/antigravity_provider/router/security_guard.py index 8bfb901..186b7bb 100644 --- a/src/antigravity_provider/router/security_guard.py +++ b/src/antigravity_provider/router/security_guard.py @@ -321,7 +321,17 @@ class WorkspaceBoundaryGuard: return False, f"Команда {cmd_name} запущена в недопустимом каталоге: {reason}", alt else: for target_arg in targets: - target_path = (base_cwd / target_arg) if not Path(target_arg).is_absolute() else Path(target_arg) + # Тильда и переменные окружения раскрываются ДО проверки. + # + # Без этого "rm -rf ~/.hermes/agy_profiles" не считался + # абсолютным путём, склеивался с каталогом проекта в путь с + # буквальным "~" внутри и признавался допустимым. Проверено: + # команда с тильдой проходила, та же команда с абсолютным + # путём отклонялась. То есть самый естественный способ + # написать опасную команду обходил защиту ровно там, ради + # чего она и делалась — на каталоге учётных данных. + expanded = os.path.expandvars(os.path.expanduser(target_arg)) + target_path = Path(expanded) if Path(expanded).is_absolute() else (base_cwd / expanded) ok, reason, alt = self.validate_path(target_path, operation="delete") if not ok: return False, f"Команда '{cmd_name}' пытается удалить недопустимый путь '{target_arg}': {reason}", alt diff --git a/tests/test_a37_isolation_guards.py b/tests/test_a37_isolation_guards.py index 216bad9..731227e 100644 --- a/tests/test_a37_isolation_guards.py +++ b/tests/test_a37_isolation_guards.py @@ -366,3 +366,33 @@ class TestCredentialsDeletionGuard: @pytest.fixture def client(): return TestClient(app) + +def test_destructive_command_with_tilde_is_rejected(monkeypatch, tmp_path): + """Команда с тильдой не должна обходить защиту каталогов учётных данных. + + validate_command не раскрывал "~" и "$HOME" перед проверкой. Путь + "~/.hermes/agy_profiles" не считался абсолютным, склеивался с каталогом + проекта в путь с буквальным "~" внутри и признавался допустимым. + + Измерено на реализации: "rm -rf ~/.hermes/agy_profiles" проходило, а та же + команда с абсолютным путём отклонялась. То есть самый естественный способ + написать опасную команду обходил защиту ровно в том месте, ради которого + она и делалась. + """ + from antigravity_provider.router.security_guard import WorkspaceBoundaryGuard + + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + guard = WorkspaceBoundaryGuard() + + must_reject = [ + "rm -rf ~/.hermes/agy_profiles", + "rm -rf ~/.ssh", + "rm -rf $HOME/.hermes", + ] + for cmd in must_reject: + allowed, reason, _alt = guard.validate_command(cmd) + assert not allowed, f"команда с тильдой прошла мимо защиты: {cmd} ({reason})" + + # Обычная работа внутри проекта не должна страдать. + allowed, _reason, _alt = guard.validate_command("rm src/temp_file.py") + assert allowed, "защита мешает штатной работе внутри проекта"