Files
HY2XS_flamy/tools/build/lib/security.sh
T
founder b99be7d514 fix(v1): разблокировать сборку, починить жизненный цикл cron и закрыть каналы утечки
Сборка не собиралась: два контракта приёмки роняли её на корректном коде.

verify_api_namespace_contract искал возвращение legacy-пространства имён
через grep по '/hui' и находил router_test.go, который ПЕРЕЧИСЛЯЕТ этот
префикс, чтобы доказать отсутствие маршрута, и сам versions.sh, где строка
стоит в тексте проверки. Падение приходило шестым шагом из четырнадцати, до
резолва Hysteria. За ним прятался второй такой же: проверка транзакционности
импорта пиров брала файл от начала applyPeerImportEntry и до конца, захватывая
объявленные ниже ExistPeerName и UpdatePeerLastConnectionAt.

Обе проверки теперь смотрят на код, а не на упоминания: добавлены помощники
code_without_comments и code_mentions_in, а отсутствие legacy-маршрута
доказывает тест на таблице маршрутов собранного роутера.

Планировщик стал собственностью процесса. InitCron вызывался из runServer и
на каждом вызове создавал новый cron.New(), не сохраняя ссылку; cron.Stop()
не вызывался нигде. Смена RESET_TRAFFIC_CRON выполняла StopServer(), точка
входа крутила for { runServer() } — и каждая правка добавляла целый
дублирующий набор джоб, а старое расписание сброса продолжало работать.
Фиксированные джобы регистрируются один раз, расписание переносится на месте
по EntryID, HTTP-сервер не трогается. Добавлено штатное завершение по SIGTERM.

Выражение проверяется до записи в базу тем же парсером (cron.ParseStandard),
которым его разбирает планировщик: раньше невалидная строка сохранялась, API
отвечал успехом, а сброс трафика молча исчезал.

updateConfigs стал атомарным: полная проверка партии, одна транзакция,
применение к рантайму. Прежний тест ставил запрещённый ключ первым и не
смотрел в базу — поймать частичное применение он был неспособен.

Удалены четыре ключа таблицы config без единого потребителя: HYSTERIA2_ENABLE,
HYSTERIA2_CONFIG (второй источник истины, читался первым), HYSTERIA2_TRAFFIC_TIME
и HYSTERIA2_CONFIG_REMARK. Имя профиля в share URI выводится из имени пира.

Безопасность:
- bootstrap-пароль администратора больше не генерируется и не пишется в журнал,
  который отдаётся кнопкой выгрузки; отсутствие env — отказ старта;
- собственный журнал админки санитизируется наравне с чужим;
- golang-jwt/jwt v3 -> v5: GO-2025-3553 не имеет исправленной версии в v3 и
  достижима с неаутентифицированного запроса; набор алгоритмов подписи
  зафиксирован через WithValidMethods;
- удалён вход по несолёному SHA-224 из предыдущего поколения;
- убран modulo bias в util.RandomString — единственном генераторе секретов;
- пир установщика защищён во всех путях записи, а не только в импорте;
- удалена латентная паника в service.GetToken и недостижимая ветка GetAdminInfo,
  проверявшая меньше, чем middleware.

Toolchain: Go 1.21.13 -> 1.26.7, Node 20.19.0 (EOL) -> 24.20.0. На прежнем
графе govulncheck находил 21 вызываемую уязвимость, 17 из них в stdlib,
попадающей в production-бинарь. Сейчас — ноль. Добавлен обязательный шаг
проверки зависимостей (govulncheck + pnpm audit) с записью результата в
metadata пакета.
2026-08-29 21:37:50 +05:00

131 lines
6.7 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# Проверка графа зависимостей на известные уязвимости.
#
# Зачем отдельный шаг сборки, а не разовая проверка «когда вспомним».
#
# Версии toolchain и библиотек фиксируются в versions.env, go.sum и lock-файлах
# — то есть намеренно НЕ движутся. Это правильно для воспроизводимости и ровно
# поэтому опасно для безопасности: зафиксированный граф не стареет только на
# бумаге, а advisory по нему выходят и после фиксации. Единственный момент,
# когда расхождение между «что мы собираем» и «что про это известно» можно
# поймать дёшево, — сама сборка релиза.
#
# История, ради которой шаг появился: на Go 1.21.13 и графе поколения 0.x
# govulncheck находил 21 ВЫЗЫВАЕМУЮ уязвимость, включая GO-2025-3553 в
# github.com/golang-jwt/jwt v3 — с пометкой `Fixed in: N/A` и путём вызова из
# разбора токена, то есть достижимую с неаутентифицированного запроса. Ни один
# из существовавших шагов сборки этого показать не мог.
#
# Проверяются РАЗНЫЕ вещи в двух экосистемах:
#
# govulncheck — анализ достижимости: уязвимость считается только если до неё
# есть путь вызова из нашего кода. Покрывает и stdlib, то есть
# ровно то, что версия Go привносит в production-бинарь;
# pnpm audit — граф npm без анализа достижимости, поэтому ограничен
# production-зависимостями и порогом PNPM_AUDIT_LEVEL:
# devDependencies в артефакт не попадают.
# Аварийный выход существует по той же причине, что и ALLOW_DIRTY_BUILD: новое
# advisory может выйти в неудачный момент, и решение «выпускаем, зная о нём»
# должно приниматься человеком ЯВНО, а не пропуском шага по умолчанию.
ALLOW_VULNERABLE_DEPENDENCIES="${ALLOW_VULNERABLE_DEPENDENCIES:-false}"
security_gate_failed() {
local scanner="$1"
local details="$2"
if [ "$ALLOW_VULNERABLE_DEPENDENCIES" = "true" ]; then
log_info "WARNING: $scanner нашёл уязвимости, но сборка продолжена по ALLOW_VULNERABLE_DEPENDENCIES=true"
log_info "$details"
DEPENDENCY_SECURITY_GATE="accepted-risk"
export DEPENDENCY_SECURITY_GATE
return 0
fi
fail "$scanner: найдены уязвимости в зависимостях. Обновите граф или, осознанно принимая риск, выставьте ALLOW_VULNERABLE_DEPENDENCIES=true.
$details"
}
# Анализ Go-графа, включая stdlib выбранной версии Go.
run_go_vulnerability_gate() {
local ui_src="${UI_SRC:-apps}"
local report status
log_step "Security: govulncheck ${GOVULNCHECK_VERSION} (Go ${GO_VERSION})"
# GOTOOLCHAIN=local обязателен: без него go может молча скачать другую
# версию toolchain, и проверялась бы не та stdlib, которая попадёт в бинарь.
set +e
report="$(cd "$ui_src" && GOTOOLCHAIN=local "$GO_BIN" run \
"golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}" ./... 2>&1)"
status=$?
set -e
# 0 — чисто; 3 — найдены вызываемые уязвимости; остальное — отказ самого
# инструмента, и его нельзя трактовать как «уязвимостей нет».
case "$status" in
0)
log_info "govulncheck: вызываемых уязвимостей не найдено"
;;
3)
security_gate_failed "govulncheck" "$report"
;;
*)
fail "govulncheck завершился с кодом $status (это отказ инструмента, а не результат проверки):
$report"
;;
esac
}
# Анализ production-зависимостей frontend.
run_frontend_vulnerability_gate() {
local ui_src="${UI_SRC:-apps}"
local report status
log_step "Security: pnpm audit --prod (порог ${PNPM_AUDIT_LEVEL})"
set +e
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --prod --audit-level "$PNPM_AUDIT_LEVEL" 2>&1)"
status=$?
set -e
if [ "$status" -eq 0 ]; then
log_info "pnpm audit: уязвимостей уровня ${PNPM_AUDIT_LEVEL} и выше не найдено"
return 0
fi
# pnpm audit ходит в реестр npm. Недоступность реестра — это отказ проверки,
# а не её отрицательный результат, и молча пропускать его нельзя.
if printf '%s' "$report" | grep -qiE 'ERR_PNPM_AUDIT_ENDPOINT|ENOTFOUND|ECONNREFUSED|network|getaddrinfo'; then
fail "pnpm audit не смог обратиться к реестру npm — проверка не выполнена:
$report"
fi
security_gate_failed "pnpm audit" "$report"
}
# Результат шага уезжает в metadata/package.env: по готовому tarball должно
# быть видно, проверялся он или собран с пропущенной проверкой. Ровно так же
# устроен hysteria_compat_gate.
run_dependency_security_gate() {
if [ "${SKIP_SECURITY_SCAN:-false}" = "true" ]; then
log_info "WARNING: проверка зависимостей на уязвимости пропущена (SKIP_SECURITY_SCAN=true)"
DEPENDENCY_SECURITY_GATE="skipped"
export DEPENDENCY_SECURITY_GATE
return 0
fi
[ -n "${GOVULNCHECK_VERSION:-}" ] \
|| fail "security: GOVULNCHECK_VERSION не задан; load_versions_contract должен выполниться первым"
[ -n "${PNPM_AUDIT_LEVEL:-}" ] \
|| fail "security: PNPM_AUDIT_LEVEL не задан; load_versions_contract должен выполниться первым"
DEPENDENCY_SECURITY_GATE="true"
export DEPENDENCY_SECURITY_GATE
run_go_vulnerability_gate
run_frontend_vulnerability_gate
}