Читать 8 мин

Интерполяция shell: как не дать вводу агента превратиться в команды

Интерполяция shell превращает управляемые агентом пути и имена ресурсов в исполняемый синтаксис. Создавайте безопасные локальные действия и команды SSH с массивами аргументов и валидацией.

Интерполяция shell: как не дать вводу агента превратиться в команды

Агенту не нужен инструмент shell, чтобы запустить выполнение команд через shell. Достаточно передать ему путь, ветку, тег образа, имя хоста или ресурса, а затем где-то ниже по цепочке вставить это значение в строку команды. Так недоверенный текст получает возможность изменить выполняемую программу.

Решение не в более хитрой функции экранирования кавычек. Нужно перестать воспринимать построение команд как форматирование строк. Действие должно задавать фиксированную операцию, принимать типизированные поля с узкими правилами, запускать программу через настоящий вектор аргументов и отделять авторизацию от разбора данных. Я просмотрел достаточно отчетов об инцидентах, чтобы сказать прямо: инструмент, который принимает произвольную команду под предлогом «это только для нашего агента», рано или поздно примет и чьи-то чужие инструкции.

Агенты делают такую ошибку более вероятной. Они обрабатывают тексты задач, содержимое репозиториев, вывод инструментов, веб-страницы и сообщения в чатах. Любой из этих источников может повлиять на следующий аргумент. Намеревался ли агент причинить вред, не имеет значения. Граница, ведущая к операционной системе или удаленному хосту, должна исходить из того, что атакующий способен управлять входными данными.

Интерполяция shell превращает данные в синтаксис

Интерполяция shell происходит, когда код строит текст для интерпретатора команд вместо передачи отдельных аргументов через API процесса. Получив такой текст, shell применяет собственные языковые правила. Разделители могут запустить другую команду, подстановки команд - вложенную команду, перенаправления - изменить файлы, а подстановки могут превратить одно видимое значение в несколько слов.

Представим, что агент должен проверить ветку перед выпуском. Обработчик выглядит безобидно:

branch = request["branch"]
command = f"git show {branch}"
subprocess.run(command, shell=True, check=True)

Если branch равен release; id, shell увидит две команды. Первая попросит Git показать ревизию. Вторая запустит id. Та же ошибка встречается в JavaScript с exec, в Ruby со строкой, переданной в system, в Go с sh -c и в CI-скриптах, которые помещают вывод агента прямо в переменную shell, не контролируя ее дальнейшее использование.

Опасный символ не обязан находиться в промпте агента. Он может быть в заголовке pull request, сообщении об ошибке, манифесте пакета или имени файла, полученном при сканировании репозитория. Агент может передать этот текст в следующий вызов инструмента, потому что задача велела ему проверить или исправить его. Проверка лишь того, подчиняется ли модель инструкции, упускает место, где текст превращается в исполняемый синтаксис.

POSIX Shell Command Language описывает последовательность, включающую подстановки, разбиение на поля в применимых случаях, раскрытие имен путей и удаление кавычек. Порядок важен. Разработчик часто говорит: «Я заключил значение в кавычки», словно кавычки создают универсальную безопасную строку. Это не так. Они ограничивают определенные правила разбора в конкретном shell и на конкретном этапе последовательности подстановок.

Кроме того, shell может получать данные разными способами. Значение способно повлиять на сам текст команды, присваивание переменной окружения, цель перенаправления, подстановку команды, файл запуска shell или код, который позднее обработает другой интерпретатор. Экранирование для первого shell автоматически не защищает от следующего парсера.

Важно различать следующие случаи:

  • Внедрение команды происходит, когда входные данные меняют синтаксис, интерпретируемый языком команд.
  • Внедрение аргумента происходит, когда значение остается одним аргументом, но меняет способ его интерпретации вызываемой программой, часто превращаясь в параметр или выражение.
  • Ошибка авторизации происходит, когда корректный аргумент запрашивает действие за пределами разрешений вызывающей стороны.

Вектор аргументов устраняет первый класс проблем. Второй и третий он не решает. Путь, выходящий за пределы рабочей области, ревизия Git, выбирающая непредусмотренный объект, и URL, обращающийся к внутреннему сервису, могут быть отдельными аргументами.

В строке команды все равно слишком много парсеров

Экранирование входных данных перед конкатенацией в команду shell популярно, потому что выглядит небольшим исправлением. Но оно хрупко: команда часто проходит через несколько грамматик.

Представим, что локальная программа строит вызов SSH, а удаленная система запускает shell. Разработчик может заключить ветку в кавычки для локального shell, еще раз для SSH и в третий раз для удаленного shell. Каждый слой по-разному трактует пробелы, кавычки, обратные слеши, подстановки команд и кодировку. Позднейшее изменение может убрать один слой кавычек или поместить уже экранированную строку в новый контекст. Код все еще выглядит аккуратным, хотя защита уже исчезла.

Особенно опасен такой шаблон:

const command = `git checkout '${branch}'`;
execFile("ssh", [host, command], callback);

execFile защищает локальный вызов ssh, и это хорошо. Но он не делает command безопасной на удаленной стороне. SSH обычно передает удаленной стороне строку команды. Удаленная учетная запись обычно передает эту строку shell. Поэтому ветка попадает в синтаксис shell на один переход позже.

Одинарные кавычки не являются универсальным решением. Входные данные с одинарной кавычкой могут завершить намеченную область цитирования. Функция, написанная для POSIX shell, будет неправильной для PowerShell. Функция, корректная для одного слова shell, не подходит, если значение попадает в перенаправление, арифметическое выражение или интерпретатор языка. Даже корректная сегодня функция становится обузой при сопровождении, когда кто-то меняет git show на git log --format=... и перестраивает шаблон.

Не путайте сериализацию списка аргументов для журналирования с восстановлением списка для выполнения. Строка журнала вроде git show release-42 удобна человеку, но не показывает, где проходили исходные границы аргументов. Это не безопасное исполняемое представление. Храните аргументы как структурированный массив, а для отображения выводите их только с понятными правилами экранирования.

Переменные окружения создают еще одну ловушку. Это безопаснее интерполяции:

BRANCH="$branch" git show "$BRANCH"

Но безопасность сохраняется только при условии, что скрипт сам владеет обоими экранированными раскрытиями и никогда не вычисляет значение позднее. Если последующий скрипт использует eval, строит еще одну строку команды или передает значение шаблонизатору с возможностью выполнения, исходное исправление мало что дало. Лучше вообще не передавать значение агента shell.

Фиксированные исполняемые файлы и массивы аргументов убирают грамматику shell

Для локальных действий вызывайте фиксированный исполняемый файл с массивом аргументов и не используйте shell. Тогда операционная система передаст дочернему процессу именно заданные строки. Она не будет интерпретировать точки с запятой, знаки доллара, пробелы, скобки или символы glob как язык shell.

Документация Python subprocess рекомендует последовательность аргументов и указывает, что shell=False используется по умолчанию. Применяйте это значение сознательно, а не случайно:

import subprocess

result = subprocess.run(
    ["git", "status", "--porcelain=v1"],
    cwd=repo_dir,
    text=True,
    capture_output=True,
    check=True,
)
print(result.stdout)

В этом действии агент не управляет ни исполняемым файлом, ни шаблоном команды. Выполняется одна известная операция в одном разрешенном каталоге. Формат вывода прост: каждая запись о состоянии находится на отдельной строке. Интерфейс можно улучшить, разобрав этот вывод и вернув структурированные записи, а не передавая необработанный текст следующему решению агента.

Если операции нужны входные данные, оставьте исполняемый файл и операцию фиксированными, а проверенные значения добавляйте отдельными элементами:

subprocess.run(
    ["tool", "fetch-resource", resource_id],
    cwd=workspace,
    check=True,
    shell=False,
)

В Node.js предпочитайте spawn или execFile с массивом аргументов. Не включайте параметр shell, чтобы упростить составную команду. В Go используйте exec.Command, передавая каждый аргумент отдельно. В Rust используйте Command и последовательные вызовы arg. API различаются, но инвариант один: ни один вход не должен попадать в язык команд.

Здесь стоит возразить против распространенного совета: «Используйте shell-скрипт как безопасную оболочку». Фиксированный shell-скрипт может быть приемлемой границей совместимости, но сам по себе скрипт не безопаснее. Он становится безопасным только тогда, когда получает значения через фиксированные позиционные параметры или стандартный ввод, заключает каждое раскрытие в кавычки, избегает eval и не строит вторую строку команды. Небольшую программу с API процессов обычно проще проверять.

У shell есть полезные возможности: конвейеры, условная логика, перенаправления и встроенные команды. Если отказаться от них нельзя, поместите их в проверенный статический скрипт. Не позволяйте агенту собирать конвейер. Дайте ему действие с именем collect_build_logs, а оболочке оставьте фиксированный конвейер, фиксированные расположения файлов и ограниченные параметры.

Сам исполняемый файл также должен входить в политику. Никогда не выбирайте его из входных данных агента, доступного для записи файла конфигурации или поиска по управляемому атакующим PATH. Если среда требует такой защиты, используйте абсолютный путь, контролируемый администратором. Задайте известный рабочий каталог и ограниченное окружение, не наследуйте произвольные переменные из процесса агента.

Валидация определяет смысл, который может иметь действие

Массивы аргументов защищают границу парсера. Валидация защищает границу действия. Если действие называется read_file, сервис должен сам определить, какие файлы оно может читать. API процесса этого решения за вас не примет.

Начните с узкой схемы. Имя развертывания может состоять из строчных букв, цифр и одиночных внутренних разделителей, с ограничением длины. Селектор ветки может быть именем ветки в выбранном подмножестве правил Git. Идентификатор ресурса может быть UUID или числом из инвентаря. Целью сервера может быть ID, который сервис сопоставляет с сохраненным хостом, а не произвольное имя хоста.

Избегайте общих черных списков вроде «отклонять точку с запятой и амперсанд». Они рассчитаны на один парсер и не защищают от смыслового злоупотребления. Со временем они разрастаются, и обычным пользователям становится непонятно, что допустимо. Грамматика со списком разрешенных форм дает вызывающей стороне договор и ограничивает набор входных данных для проверки.

Для поля ветки в процессе выпуска намеренно ограниченная грамматика может быть лучше, чем прием любой ссылки, которую поддерживает Git:

import re

BRANCH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/]{0,127}")

def release_ref(value: str) -> str:
    if not isinstance(value, str):
        raise ValueError("branch must be text")
    if not BRANCH.fullmatch(value):
        raise ValueError("branch has unsupported characters")
    if "/./" in value or "//" in value or value.endswith("/"):
        raise ValueError("branch has an unsupported path form")
    return "refs/heads/" + value

Этот код не заявляет, что реализует полную грамматику ссылок Git. Он намеренно задает меньшую грамматику для действия выпуска. Часто это правильное инженерное решение. Если пользователям нужны пробелы или другая необычная форма, добавьте ее потому, что этого требует процесс, протестируйте и задокументируйте. Не наследуйте все крайние случаи общей системы контроля версий, если ваше действие разворачивает только ветки выпуска.

Возвращаемое значение имеет фиксированный префикс пространства имен. Это важно. Произвольное выражение ревизии может ссылаться на теги, историю предков, reflog или синтаксис, который Git интерпретирует специально. Действие для ветки выпуска не должно незаметно превращаться в «показать любой объект Git, который агент способен описать». Если нужна полная совместимость с Git, используйте библиотеку или встроенный режим проверки веток Git, но все равно сопоставляйте результат с разрешенным пространством имен.

Та же дисциплина нужна именам ресурсов. Если агент запрашивает облачный объект, храните разрешенные учетную запись, регион, bucket или проект вне свободного поля. Пусть агент передает имя объекта, соответствующее единственной поддерживаемой грамматике. Не принимайте полный URL под видом имени ресурса. Полный URL выбирает протокол, хост, порт, путь, а иногда и учетные данные. Это решение о сетевой авторизации, замаскированное под строку.

Ограничения длины тоже часть валидации. Они ограничивают рост журналов, предотвращают случайное превышение ограничений командной строки и усложняют отказ в обслуживании. Правила кодировки также важны. Отклоняйте управляющие символы в текстовых полях, если действие действительно не требует их. NUL всегда отклоняйте в аргументах процессов: интерфейсы запуска процессов операционной системы не могут передать его как часть аргумента.

Для путей нужны проверки принадлежности, а не фильтры символов

Проверять аудит офлайн
Проверяйте цепочку аудита офлайн по шифротексту с помощью sp audit verify, без ключа хранилища.

Путь может быть безопасным для shell и опасным для файловой системы. ../../secrets.env не содержит метасимволов shell, о которых обычно беспокоятся функции экранирования. Но он может вывести файловую операцию за пределы рабочей области.

Безопасная политика путей начинается с объявления корневого каталога действия. Разрешите запрошенный относительный путь относительно этого корня, затем убедитесь, что полученная цель остается внутри разрешенного корня. Не принимайте абсолютный путь, если действию нужны только файлы рабочей области. Не нормализуйте строку и не считайте, что ее итоговое написание показывает, куда направится файловая система.

Концептуальная проверка выглядит так:

from pathlib import Path

workspace = Path("/srv/agent-workspaces/repo-a").resolve()

def permitted_file(relative_name: str) -> Path:
    if not isinstance(relative_name, str) or relative_name.startswith("/"):
        raise ValueError("file must be a relative path")
    candidate = (workspace / relative_name).resolve(strict=True)
    if candidate == workspace or workspace not in candidate.parents:
        raise ValueError("file is outside the workspace")
    if not candidate.is_file():
        raise ValueError("requested target is not a regular file")
    return candidate

Вызов resolve обнаруживает обычный обход каталогов и учитывает существующие символические ссылки. Это лучше, чем проверять, содержит ли исходная строка ... Но для чувствительной записи этого все равно недостаточно. Атакующий, способный менять дерево каталогов, может заменить компонент пути символической ссылкой после проверки и до последующего открытия. Это разрыв между проверкой и использованием, то есть гонка time-of-check to time-of-use.

Для чтения в контролируемой рабочей области проверка принадлежности после разрешения пути может соответствовать вашему уровню риска. Для записи, удаления, изменения прав или архивов, на которые может влиять атакующий, используйте файловые операции с дескрипторами каталогов и запретом обхода символических ссылок, если операционная система предоставляет такие средства. Отделяйте доступные агенту рабочие области от кода, определяющего разрешенные корни. Если модель угроз включает локального атакующего, способного создавать гонки файловой системы, одной высокоуровневой библиотеки путей недостаточно.

Также решите, входят ли в область действия dot-файлы, метаданные репозитория, каталоги с созданными зависимостями и символические ссылки. Расплывчатое правило вроде «все внутри репозитория» часто открывает файлы с учетными данными, кэши сборки или конфигурацию, которая процессу не нужна. Доступ к чтению должен соответствовать цели действия, а не удобству рекурсивного glob.

Извлечение архивов требует отдельного предупреждения. Проверяйте каждый элемент архива после присоединения к корню назначения и считайте ссылки внутри архива потенциально опасными. Обход каталогов ограничивается не только запросами, которые агент делает напрямую. Инструмент, извлекающий выбранный агентом артефакт, может выполнить такой же выход от его имени.

Параметры и выражения опасны даже без shell

Вектор аргументов отделяет значение от синтаксиса shell, но вызываемая программа все равно разбирает это значение. Многие программы считают аргументы, начинающиеся с дефиса, параметрами. Некоторые принимают выражения, ссылки на конфигурацию, синтаксис включения файлов, плагины или хуки команд. Передача недоверенного ввода одним аргументом не делает такой смысл безопасным.

Представим оболочку, которая вызывает программу поиска с шаблоном пользователя. Оболочка может правильно передать ["search", pattern, directory]. Но если программа считает шаблон регулярным выражением, патологичное выражение способно потребить огромное количество CPU. Если она поддерживает параметр чтения файла конфигурации, а шаблон оказался не на том месте, вызывающая сторона может изменить поведение программы. Если язык программы поддерживает выполнение кода, вы передали агенту другой интерпретатор.

Размещайте фиксированные параметры перед данными и используйте стандартный маркер конца параметров, если программа его поддерживает. В текстовом виде этот маркер состоит из двух дефисов. Он сообщает многим парсерам командной строки, что последующие значения являются операндами, а не флагами. Не полагайтесь на него как на валидацию: некоторые инструменты его не поддерживают, а операнд все равно может иметь опасный смысл.

У Git большая поверхность команд, поэтому агентам нужны узкие действия Git, а не универсальный выход «запустить Git». Действие show_release_branch может принимать ограниченное имя ветки, добавлять к нему refs/heads/ и вызывать фиксированную подкоманду Git. Действие checkout_anything, принимающее произвольный синтаксис ревизии, имеет более широкий смысл и требует более широкого решения об авторизации. Это разные продукты, даже если оба запускают процесс Git.

То же относится к менеджерам пакетов, клиентам баз данных, инструментам работы с медиа, архиваторам и инфраструктурным командам. Спросите себя, какую грамматику целевая программа будет разбирать после доставки аргумента операционной системой. Командная строка часто лишь первый парсер в цепочке.

К регулярным выражениям нужен особый подход. Шаблон не является кодом shell, но это исполняемая работа внутри движка регулярных выражений. Если агенту нужно искать текст, по возможности выбирайте движок с предсказуемой производительностью, ограничивайте длину шаблона и входных данных, а литеральный поиск делайте действием по умолчанию. Не превращайте каждый запрос поиска в неограниченный язык выражений только потому, что разработчику нужен гибкий endpoint.

SSH превращает локальную команду в удаленную задачу разбора

Использовать локальный шлюз на Mac
Запускайте шлюз как одно подписанное приложение macOS в строке меню, со встроенным ядром хранилища.

SSH создает вторую границу выполнения. Можно идеально вызвать локальный SSH-клиент через массив аргументов и все равно отправить небезопасную строку команды на удаленный хост. Многие реализации SSH выполняют запрошенную удаленную команду через shell удаленной учетной записи или эквивалентный парсер команд.

Не стройте удаленные команды из полей агента. Неправильно даже так, если локальный вызов использует массив:

remote = "deploy " + environment + " " + branch
subprocess.run(["ssh", host, remote], check=True)

Безопасная схема отправляет постоянную удаленную команду, а данные передает через структурированный канал. Например, удаленная учетная запись может предоставлять одну проверенную программу по фиксированному пути. Локальная сторона запускает ее, отправляет JSON-запрос в стандартный ввод, а удаленная программа проверяет environment и branch до запуска любой локальной операции.

import json
import subprocess

request = {"environment": "staging", "branch": "release/42"}
subprocess.run(
    ["ssh", "deploy.internal", "/usr/local/libexec/receive-deploy-request"],
    input=json.dumps(request) + "\n",
    text=True,
    check=True,
)

В этом примере удаленная команда постоянна. JSON передается через стандартный ввод как данные, а не встраивается в удаленную команду. Удаленный приемник все равно должен отклонять неизвестные поля, ограничивать длину, проверять каждое значение и использовать массив аргументов для любого дочернего процесса. Структурированный транспорт убирает лабиринт кавычек, но не создает доверие.

Имя хоста обычно следует выбирать из сохраненной конфигурации по непрозрачному ID окружения. Если позволить агенту передавать host, он сможет выбирать, куда уйдут учетные данные и какую сетевую границу пересечет действие. Если процесс действительно требует несколько хостов, сопоставляйте утвержденные имена вроде staging и production с известными настройками подключения. Не отключайте проверку хоста. Инструмент, отключающий ее ради удобства настройки, убирает важную часть границы защиты.

Не отправляйте учетные данные в удаленную команду, командную строку или JSON-запрос. Используйте существующую схему аутентификации удаленной учетной записи и ограничьте эту учетную запись необходимым действием. Приемник развертывания, способный развернуть только одно приложение, проще контролировать, чем агент, получающий универсальную shell-сессию.

Подтверждение не исправляет небезопасную архитектуру действия

Проверять чувствительные вызовы по отдельности
Отметьте ключ для подтверждения при каждом использовании, одним нажатием или через Touch ID.

Подтверждение человеком и журнал аудита полезны, но отвечают на другие вопросы, чем валидация. Подтверждение отвечает, может ли этот вызывающий процесс сейчас попытаться выполнить действие. Валидация отвечает, имеет ли запрос определенную и разрешенную форму. Журнал отвечает, что произошло и не изменились ли записи позднее. Подмена одного контроля другим создает слабые инструменты с привлекательными экранами.

Однократное подтверждение сеанса может подходить для фиксированного шлюза действий. Но это не проверка каждой интерполированной строки, которую процесс в течение сеанса передаст shell. Подтверждение каждого вызова дает человеку больше возможностей заметить странную цель, но нельзя ожидать, что он будет проверять вложенные правила кавычек или искать вредоносный похожий символ Unicode в плотном диалоге.

Пусть карточка подтверждения показывает смысловой запрос: deploy branch release/42 to staging, а не восстановленную из шаблона shell-команду. Приемник должен уметь сравнить показанные окружение и ветку со схемой запроса. Если для объяснения команда требует поля с необработанным скриптом, действие слишком широко для надежного подтверждения.

Записывайте структурированные факты на границе действия. Сохраняйте идентификатор вызывающей стороны или процесса агента, подтвержденное имя действия, отдельные аргументы или поля запроса, выбранную цель, время, код завершения и ссылки на вывод с учетом правил хранения данных. Записывайте и ошибки валидации. Повторяющиеся отклоненные строки с обходом каталогов или значениями, похожими на параметры, часто указывают на плохой промпт, ошибку интеграции или активное исследование системы.

Для команд, использующих Sallyport, авторизация на сеанс, подтверждение ключа при каждом вызове и отдельные журналы Sessions и Activity помогают сохранять контроль и свидетельства вокруг действий агентов. Но к шаблону команды все равно применяются описанные выше правила проектирования.

Защищенный от подделки журнал помогает после инцидента только тогда, когда записывает важные границы. Строка вроде ssh deploy internal deploy staging release 42 неоднозначна задним числом. Событие с отдельными идентификатором хоста, постоянным путем приемника, полями JSON, результатом авторизации и кодом завершения приемника может поддержать расследование.

Проверяйте путь отказа так же тщательно, как успешный путь

Большинство тестов внедрения команд доказывает лишь, что обычная ветка вроде release/42 работает. Это наименее интересный случай. Набор тестов должен показывать, что обработчик отклоняет опасные значения до запуска дочернего процесса или открытия пути.

Для каждого типа поля составьте небольшую таблицу. Конкретные значения зависят от грамматики, но категории должны включать разделители shell, подстановки команд, кавычки, пробелы и переводы строк, начальные символы параметров, компоненты обхода каталогов, пустые значения, управляющие символы, слишком длинные значения и корректный синтаксис, запрашивающий действие за пределами области. Для действия с веткой проверьте также имя, похожее на тег, и выражение ревизии, которое интерфейс не поддерживает.

В модульных тестах используйте поддельный запускатель процессов. Проверяйте исполняемый файл и каждый аргумент как отдельное значение. Проверяйте, что некорректный ввод вообще не вызывает запускатель. Тест, сравнивающий только отрисованную строку команды, может пропустить именно ту границу, которую вы защищаете.

class RecordingRunner:
    def __init__(self):
        self.calls = []

    def run(self, argv, **kwargs):
        self.calls.append((argv, kwargs))

def test_rejects_branch_separator():
    runner = RecordingRunner()
    try:
        release_ref("release/42; id")
    except ValueError:
        pass
    else:
        raise AssertionError("expected rejection")
    assert runner.calls == []

Этот тест проверяет функцию политики, а не shell. Добавьте и интеграционный тест границы процесса. Запустите безвредную тестовую программу, которая выводит полученные аргументы с явными индексами и длинами. Передайте ей значения с пробелами, кавычками, wildcard-символами, скобками и переводами строк. Убедитесь, что каждое значение пришло одним аргументом и что вторая программа не запустилась.

Фаззинг полезен после появления схемы действия. Генерируйте случайный Unicode внутри и вне разрешенного набора и проверяйте один из двух результатов: валидатор отклоняет значение либо фиксированная операция получает его как одно ограниченное поле. Не проводите такие проверки против рабочего развертывания. Цель - заранее найти предположения о парсерах в шлюзе и приемнике, до того как агент доберется до настоящего окружения с учетными данными.

Наконец, проверьте все места, где структурированные данные снова превращаются в текст. Сюда относятся средства просмотра журналов с кликабельными командами, создаваемые shell-фрагменты, конфигурация CI, отчеты об ошибках, уведомления в чате и удаленные оболочки. Ошибки внедрения часто возвращаются, когда безопасный структурированный запрос становится удобной строкой в следующем компоненте.

Обычно первым стоит исправить самое гибкое действие. Замените run_command(command) одной операцией с именем, небольшой схемой запроса, фиксированным исполняемым файлом и набором тестов с вводом, который никто не стал бы вводить во время обычной демонстрации. Это убирает целый класс поведения модели из вашей модели угроз и оставляет правила, которые действительно можно проверить.

Вопросы и ответы

Достаточно ли кавычек в shell, чтобы предотвратить внедрение команд?

Кавычки меняют способ, которым один shell разбирает значение. Они не определяют, может ли это значение обозначать разрешенный файл, ветку, хост или ресурс API. Считайте кавычки защитой границы парсера, а валидацию проектированием разрешений: нужны оба механизма.

Можно ли разрешать AI-агенту использовать sh -c?

Обычно нет. Используйте API запуска процессов, который принимает исполняемый файл и массив аргументов, а выполнение через shell отключите. Это убирает грамматику shell, но аргументы все равно нужно проверять с учетом смысла вызываемой программы.

Может ли внешне безопасный путь причинить вред?

Путь вроде ../../private/config не содержит метасимволов shell, но все равно может выйти за пределы нужного каталога. Разрешайте путь относительно утвержденного корня, проверяйте принадлежность после разрешения пути и учитывайте символические ссылки до открытия файла или выполнения операции.

Безопасны ли массивы аргументов, если команда выполняется через SSH?

Нет. Раздельный аргумент остается данными для локального процесса, но SSH обычно отправляет строку команды, которую разбирает удаленный shell. Передавайте постоянную удаленную команду и структурированные данные через стандартный ввод, а затем снова проверяйте их на удаленной стороне.

Как проверять ветку Git, которую передает агент?

Отклоняйте имена, не соответствующие грамматике, которую действительно поддерживает ваш процесс, затем преобразуйте принятое имя в фиксированное пространство имен, например refs/heads/ плюс это имя. Не принимайте произвольные ревизии, выражения ссылок или параметры команд только потому, что Git умеет их интерпретировать.

Почему косвенное внедрение в промпт важно для shell-команд?

Внедрение в промпт не обязано выглядеть злонамеренно. Файл репозитория, описание задачи, журнал сборки или ответ API могут заставить агента передать опасное значение инструменту. Граница инструмента должна сама применять свои правила, даже если агент добросовестно следует инструкциям.

Предотвращают ли диалоги подтверждения небезопасные команды агента?

Подтверждение говорит, что процессу разрешено сделать вызов. Оно не превращает небезопасный шаблон команды в безопасный и не доказывает, что аргумент входит в допустимую область. Сначала спроектируйте безопасное действие, а уже затем просите человека его подтвердить.

Что должен записывать аудит команд агента?

Полезная запись аудита содержит исполняемый файл, каждый аргумент отдельным полем, идентификатор вызывающего процесса, контекст цели, результат и хеш либо безопасно сохраненную копию структурированных данных. Одна восстановленная строка команды не сохраняет границы, которые понадобятся при расследовании.

Нужно ли блокировать специальные символы или использовать список разрешенных значений?

Отклонение любых необычных символов безопасно только в том случае, если продукт может работать с настолько ограниченной грамматикой. Лучше использовать список разрешенных форм, соответствующий предметной области, например простым именам развертываний или веткам репозитория, и документировать их. Не притворяйтесь, что общий черный список понимает все участвующие парсеры.

Как тестировать шлюз действий агента на внедрение команд?

Начните с инструмента, который принимает одну ограниченную операцию, один структурированный запрос и не содержит поля для shell-команды. Проверьте разделители, подстановки, начальные символы параметров, обход каталогов, переводы строк и значения, которые синтаксически допустимы, но выходят за область действия. Если какой-либо случай приводит к непредусмотренной операции, интерфейс слишком широк.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov