
Consul Template проверял, куда указывала символическая ссылка во время оценки шаблона, но при последующем получении зависимостей читал исходный путь. Перенаправление ссылки между этими операциями превращало ссылку на файл внутри песочницы в раскрытие файла за её пределами.
Consul Template проверял, куда указывает символическая ссылка во время вычисления шаблона, но последующее получение зависимости читало исходный путь. Перенацеливание ссылки между этими операциями превращало ссылку на файл внутри песочницы в раскрытие файла за её пределами.
Я обнаружил эту проблему при изучении HashiCorp Consul Template, задаваясь вполне конкретным вопросом безопасности:
Если sandbox_path проверяет символическую ссылку во время вычисления шаблона, остаётся ли последующее чтение файла привязанным к той же проверенной цели?
В данном случае ответ был отрицательным.
Хелпер шаблона file разрешал путь и обеспечивал соблюдение настроенной песочницы во время вычисления шаблона. Но после этой проверки он создавал файловую зависимость, используя исходный «сырой» путь.
Эта зависимость получалась позже.
Если атакующий перенацеливал символическую ссылку в промежутке между проверкой и получением зависимости, Consul Template мог прочитать файл за пределами песочницы. Если ссылка восстанавливалась до следующего рендеринга, проверка песочницы снова проходила успешно, а кэшированное внешнее содержимое всё равно попадало в вывод.
Эта проблема получила номер CVE-2026-5061.
Бюллетень HashiCorp: HCSEC-2026-12
Бюллетень IBM: бюллетень безопасности CVE-2026-5061
CVE: CVE-2026-5061
Исправлено в: 0.42.0
photo0
attacker-controlled in-sandbox symlink -> sandbox validation resolves to safe target -> dependency stores original raw path -> attacker retargets link outside sandbox -> dependency fetch reads external file -> attacker restores safe link -> next validation passes -> cached external content is rendered
Consul Template — это инструмент рендеринга шаблонов для данных из Consul и Vault.
Он может работать непрерывно, отслеживать изменения зависимостей и выводить данные в файлы или переменные окружения для использования приложениями.
Хелпер шаблона file читает локальный файл и вставляет его содержимое в отрендеренный вывод.
Поскольку чтение локальных файлов может раскрывать секреты, доступные процессу, Consul Template предоставляет sandbox_path в качестве границы для этого хелпера.
Документированное свойство безопасности простое:
file, должны находиться внутри настроенной песочницыЭто делает sandbox_path реальной границей безопасности.
Важным был не вопрос о том, выглядел ли путь так, будто он находится внутри песочницы.
Настоящий вопрос заключался в следующем:
Остаётся ли читаемый файл тем же самым файлом, который прошёл проверку песочницы?
В уязвимых версиях — нет.
Файловые песочницы часто ломаются именно в промежутке между проверкой пути и доступом к файлу.
Типичный сценарий таков:
Это предположение небезопасно, если атакующий может изменить символическую ссылку или аналогичное перенаправление файловой системы между двумя операциями.
Consul Template делал эту поверхность атаки особенно интересной, потому что вычисление шаблона и получение зависимостей были отдельными этапами.
Это разделение породило правильный вопрос безопасности:
Привязано ли получение зависимости к проверенной цели, или оно снова разрешает путь, контролируемый атакующим?
Именно эту границу я исследовал.
Корневая причина заключалась в несоответствии типа time-of-check to time-of-use (TOCTOU) между:
template/funcs.godependency/file.goВ протестированной ревизии fileFunc() делала следующее:
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(s)
Хелпер проверки корректно разрешал символические ссылки перед проверкой нахождения в песочнице:
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))
rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("'%s' is outside of sandbox", path)
}
Итак, сама проверка песочницы учитывала разрешённую цель.
Но эта разрешённая цель отбрасывалась.
NewFileQuery() вместо неё сохраняла исходную строку:
return &FileQuery{
stopCh: make(chan struct{}, 1),
path: s,
}, nil
Затем последующее получение зависимости снова читало этот путь:
data, err := os.ReadFile(d.path)
В этом и заключается вся уязвимость.
Код проверял один результат разрешения пути в файловой системе, а затем использовал другой.
Потому что символическая ссылка изменяема.
Атакующему не нужно, чтобы цель за пределами песочницы напрямую проходила pathInSandbox().
Им нужно лишь изменить то, на что уже одобренный «сырой» путь указывает после проверки, но до того, как Fetch() выполнит чтение.
Последовательность такова:
pathInSandbox()FileQuery сохраняет «сырой» путь ссылкиos.ReadFile(d.path) снова разрешает ссылкуПоследний шаг важен.
Восстановление безопасной ссылки не удаляло уже полученный секрет из кэша зависимостей.
Ключевое отличие — обход явного механизма безопасности.
Это было не просто:
«файл изменился, пока Consul Template наблюдал за ним»
Наблюдение за файлами на предмет изменений — ожидаемое поведение.
Настоящая проблема заключалась в следующем:
путь, прошедший документированное ограничение песочницы, впоследствии мог быть использован для чтения файла за пределами этой песочницы
Это прямой отказ границы доверия.
Приложение уже приняло решение о безопасности:
Но последующее получение не было привязано к той цели, которая обосновала это решение.
Именно это превратило обычную изменчивость файловой системы в уязвимость.
Я собрал автономный воспроизводитель вокруг точной последовательности «вычисление шаблона → получение зависимости».
В контролируемой настройке использовались:
Процесс воспроизведения был таким: