
Consul Template проверял, куда указывала символическая ссылка во время оценки шаблона, но при последующем получении зависимостей читал исходный путь. Перенаправление ссылки между этими операциями превращало ссылку на файл внутри песочницы в раскрытие файла за её пределами.
Consul Template проверял, куда указывает символическая ссылка во время вычисления шаблона, но последующее получение зависимости читало исходный путь. Перенацеливание ссылки между этими операциями превращало ссылку на файл внутри песочницы в раскрытие файла за её пределами.
Я обнаружил эту проблему при изучении HashiCorp Consul Template, задаваясь вполне конкретным вопросом безопасности:
Если sandbox_path проверяет символическую ссылку во время вычисления шаблона, остаётся ли последующее чтение файла привязанным к той же проверенной цели?
В данном случае ответ был отрицательным.
Хелпер шаблона file разрешал путь и обеспечивал соблюдение настроенной песочницы во время вычисления шаблона. Но после этой проверки он создавал файловую зависимость, используя исходный «сырой» путь.
Эта зависимость получалась позже.
Если атакующий перенацеливал символическую ссылку в промежутке между проверкой и получением зависимости, Consul Template мог прочитать файл за пределами песочницы. Если ссылка восстанавливалась до следующего рендеринга, проверка песочницы снова проходила успешно, а кэшированное внешнее содержимое всё равно попадало в вывод.
Эта проблема получила номер CVE-2026-5061.
Бюллетень HashiCorp: HCSEC-2026-12
Бюллетень IBM: бюллетень безопасности CVE-2026-5061
0.42.0photo0
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 наблюдал за ним»
Наблюдение за файлами на предмет изменений — ожидаемое поведение.
Настоящая проблема заключалась в следующем:
путь, прошедший документированное ограничение песочницы, впоследствии мог быть использован для чтения файла за пределами этой песочницы
Это прямой отказ границы доверия.
Приложение уже приняло решение о безопасности:
Но последующее получение не было привязано к той цели, которая обосновала это решение.
Именно это превратило обычную изменчивость файловой системы в уязвимость.
Я собрал автономный воспроизводитель вокруг точной последовательности «вычисление шаблона → получение зависимости».
В контролируемой настройке использовались:
Процесс воспроизведения был таким:
file, чтобы проверка песочницы прошла успешно и «сырой» путь был зарегистрирован как зависимость.os.ReadFile(d.path) прочитать данные через перенаправленную ссылку.Наблюдаемое поведение было следующим:
Это сравнение хэшей имело значение.
Оно доказало, что итоговое отрендеренное значение — это не устаревшее безопасное содержимое, не артефакт имени файла и не побочный эффект пути обработки ошибок.
Это было побайтово точное содержимое файла за пределами песочницы.
Самое убедительное доказательство проблемы TOCTOU должно контролировать временную последовательность.
Простая демонстрация того, что символическая ссылка может указывать за пределы песочницы, была бы слабее, поскольку pathInSandbox() и так отклоняет внешнюю цель, когда обнаруживает её.
Утверждение о безопасности зависело от доказательства всех этих состояний по порядку:
Именно поэтому воспроизводитель явно разделял проверку шаблона, получение зависимости, восстановление ссылки и следующий рендеринг.
PoC не полагался на случайные тайминги и многократные угадывания.
Он детерминированно проходил уязвимый жизненный цикл.
Это сделало корневую причину и последствия гораздо легче обосновать.
Эта проблема требует возможности влиять на локальную файловую систему.
Атакующему нужен достаточный уровень доступа, чтобы создать, заменить или перенацелить соответствующую символическую ссылку или аналогичный связанный путь в течение уязвимого окна.
Процесс Consul Template также должен:
Эти требования важны.
Это не было неаутентифицированным удалённым чтением произвольных файлов в конфигурации по умолчанию.
Но в рамках затронутой локальной модели доверия последствия были существенными:
sandbox_pathРиск возрастает, когда процесс работает с доступом к чувствительным учётным данным, конфигурации сервисов, токенам или закрытым ключам.
HashiCorp классифицировал проблему как:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
Опубликованная базовая оценка:
4.7 / Medium
Эта оценка отражает реальные ограничения:
Это разумная классификация.
Проблема узка по предъявляемым к атакующему требованиям, но обход песочницы и результирующее раскрытие файлов — вполне конкретны.
В бюллетене HashiCorp указано:
Affected: consul-template up to 0.41.4
Fixed: consul-template 0.42.0
Исправление вышло в релизе 0.42.0 15 апреля 2026 года.
Исправление небольшое и напрямую устраняет нарушенную привязку.
Вместо проверки разрешённой цели и последующего создания зависимости из исходных данных исправленный код возвращает разрешённый путь и передаёт его в NewFileQuery():
resolvedPath, err := resolveSandboxedPath(
sandboxPath,
strings.TrimSpace(s),
)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(resolvedPath)
Свойство безопасности изменилось с:
на:
Это закрывает описанный вектор перенацеливания символической ссылки, потому что изменение исходной ссылки больше не меняет путь, сохранённый зависимостью.
Патч также добавил целенаправленный регрессионный тест, покрывающий точную последовательность:
Это именно та мера устранения, которая нужна для такого бага:
Я сообщил об этой проблеме в частном порядке в HashiCorp Security 20 марта 2026 года.
Отчёт включал:
HashiCorp исправил проблему в 0.42.0 и опубликовал HCSEC-2026-12 12 мая 2026 года.
IBM опубликовала соответствующий бюллетень безопасности для того же CVE.
Оба официальных бюллетеня указали автора отчёта:
Mohamed Abdelaal (0xmrma)
Ключевой урок прост:
проверки пути недостаточно, если последующая файловая операция может разрешить этот путь в другой объект
Это правило применимо далеко за пределами Consul Template.
Оно важно везде, где код выполняет:
Более глубокая суть безопасности не в том:
«строка пути однажды выглядела безопасной»
а в том:
объект, используемый чувствительной операцией, должен быть тем объектом, который прошёл проверку
В данном случае Consul Template проверил одно разрешение пути, а получил другое.
Этого разрыва оказалось достаточно.
sandbox_path был явной границей безопасности для локальных файловpathInSandbox() корректно разрешала и проверяла цель символической ссылкиFileQuery сохраняла исходный путь, на который влияет атакующийos.ReadFile0.42.0 исправила баг, привязав зависимость к разрешённому, проверенному путиЭта уязвимость не связана с обходом проверки строкового префикса.
Она связана со временем и идентичностью.
Consul Template проверял, куда указывала ссылка во время вычисления шаблона. Получение зависимости позже снова задало файловой системе тот же вопрос. Атакующий мог изменить ответ между этими двумя моментами.
Именно поэтому проблема получила номер CVE-2026-5061.
Исправлено в consul-template 0.42.0.