fix(security): команда с тильдой обходила защиту каталога учётных данных

Проверка 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 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-30 22:51:17 +07:00
parent c8f9b5a382
commit 88d579af08
2 changed files with 41 additions and 1 deletions

View file

@ -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

View file

@ -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, "защита мешает штатной работе внутри проекта"