From 6304063187a4573bed5b9beab04f78cd1a4e84ac Mon Sep 17 00:00:00 2001 From: ochenstarik-ui Date: Thu, 16 Jul 2026 22:18:39 +0700 Subject: [PATCH] Harden installer menu and security defaults --- .github/dependabot.yml | 6 +++ .github/workflows/bash-syntax.yml | 3 ++ README.md | 25 ++-------- SECURITY.md | 25 ++++++++++ docs/readme/README.ru.md | 26 +++------- ochenstarik-server-2.sh | 3 +- ochenstarik-server-backup-7.sh | 38 +++++++++++--- ochenstarik-server-install.sh | 79 +++++++++++++++--------------- ochenstarik-server-panel-warp-6.sh | 45 +++++++++++++++-- ochenstarik-server-uninstall.sh | 23 +++++++-- tests/test-master-language.sh | 5 ++ tests/test-security-invariants.sh | 32 ++++++++++++ tests/test-step2-ipv4-firewall.sh | 5 ++ 13 files changed, 220 insertions(+), 95 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 SECURITY.md mode change 100644 => 100755 ochenstarik-server-backup-7.sh mode change 100644 => 100755 ochenstarik-server-install.sh mode change 100644 => 100755 ochenstarik-server-panel-warp-6.sh mode change 100644 => 100755 ochenstarik-server-uninstall.sh mode change 100644 => 100755 tests/test-master-language.sh create mode 100755 tests/test-security-invariants.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/bash-syntax.yml b/.github/workflows/bash-syntax.yml index f1d0f45..48637e1 100644 --- a/.github/workflows/bash-syntax.yml +++ b/.github/workflows/bash-syntax.yml @@ -29,6 +29,9 @@ jobs: - name: Test step 2 IPv4 firewall mode run: bash tests/test-step2-ipv4-firewall.sh + - name: Test security invariants + run: bash tests/test-security-invariants.sh + - name: Test master language selection run: bash tests/test-master-language.sh diff --git a/README.md b/README.md index 6b719c9..0b09115 100644 --- a/README.md +++ b/README.md @@ -32,33 +32,17 @@ chmod 700 ./*.sh sudo ./ochenstarik-server-install.sh ``` -The first wizard prompt selects the dialog language. English is the default; Russian, Spanish, German, French, Portuguese, Simplified Chinese, Japanese, Arabic, and Hindi are also available. The wizard then presents steps 1–8 in order. Each step can be installed, skipped, or used to exit the wizard. +The first wizard prompt selects the dialog language. English is the default; Russian, Spanish, German, French, Portuguese, Simplified Chinese, Japanese, Arabic, and Hindi are also available. The wizard then opens the main menu with steps 1–8. Select any step, run it, and the wizard returns to the main menu. Keep the current SSH session open while changing the SSH port. Test the new login in a second terminal before disconnecting. -## Short wizard-only installation - -If `raw.githubusercontent.com` is reachable from the server: - -```bash -sudo apt-get update -sudo apt-get install -y curl ca-certificates -curl -4 -fLO \ - --retry 5 \ - --retry-delay 10 \ - --connect-timeout 30 \ - https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-install.sh -chmod 700 ochenstarik-server-install.sh -sudo ./ochenstarik-server-install.sh -``` - -The wizard downloads any missing module from the `main` branch and validates it with `bash -n` before execution. +The wizard intentionally requires all module scripts to be present locally. Use the full archive or `git clone`; a single downloaded wizard file will not fetch missing modules from the mutable `main` branch. ## Modules | Script | Purpose | Standalone use | | --- | --- | --- | -| `ochenstarik-server-install.sh` | Unified wizard with language selection and sequential module installation | Recommended | +| `ochenstarik-server-install.sh` | Unified wizard with language selection and a main menu for all modules | Recommended | | `ochenstarik-server-1.sh` | Timezone, terminal locale, program groups, and a 2 GiB swap file | Yes | | `ochenstarik-server-2.sh` | Base packages, SSH port, IPv4/IPv6 mode, UFW, and port management | Yes | | `ochenstarik-server-user-3.sh` | Administrative user, SSH key permissions, SSH migration, sudo, and fail2ban | Run after step 2 | @@ -69,10 +53,9 @@ The wizard downloads any missing module from the `main` branch and validates it | `ochenstarik-server-ai-agents-8.sh` | Installs selected AI agents: Hermes, OpenClaw, OpenHands, OpenCode, Aider, AutoGPT, or Pi Coding Agent | Optional | | `ochenstarik-server-uninstall.sh` | Removes project-managed settings so installation can start again | Use carefully | -Every module can be downloaded and run independently: +Every module can be run independently from the full archive or cloned repository: ```bash -curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/SCRIPT_NAME.sh chmod 700 SCRIPT_NAME.sh bash -n SCRIPT_NAME.sh sudo ./SCRIPT_NAME.sh diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e7f687b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policy + +## Supported Versions + +The `main` branch is the active development line. For production use, prefer a reviewed release archive or a pinned commit instead of running scripts directly from a mutable branch. + +## Reporting a Vulnerability + +Please report security issues privately through GitHub private vulnerability reporting when it is enabled for this repository. If that channel is unavailable, open a minimal GitHub issue that does not include exploit details or secrets, and request a private contact channel. + +Include: + +- affected script and commit SHA; +- expected and actual behavior; +- impact and required privileges; +- safe reproduction steps; +- whether any secret, host, or user data may have been exposed. + +## Installer Trust + +These scripts can run as `root`, change firewall rules, create users, configure SSH, and install third-party software. Do not execute an installer until you have reviewed the exact commit or release archive you intend to run. + +## Secrets + +Do not paste private keys, API tokens, Telegram bot tokens, VPN links, or server passwords into public issues, pull requests, screenshots, or logs. diff --git a/docs/readme/README.ru.md b/docs/readme/README.ru.md index 640c4c6..1c76adf 100644 --- a/docs/readme/README.ru.md +++ b/docs/readme/README.ru.md @@ -32,23 +32,9 @@ chmod 700 ./*.sh sudo ./ochenstarik-server-install.sh ``` -Первым запросом мастер предлагает выбрать язык диалога и только затем показывает этапы 1–8 по порядку. На каждом этапе можно выбрать установку, пропуск или завершение. В первом этапе пустой ввод устанавливает все рекомендуемые наборы программ. Во время изменения SSH-порта не закрывайте текущую сессию — сначала проверьте новый вход во втором терминале. +Первым запросом мастер предлагает выбрать язык диалога и только затем открывает главное меню со всеми этапами 1–8. Выберите нужный этап, установите его, и после завершения мастер вернётся обратно в главное меню. В первом этапе пустой ввод устанавливает все рекомендуемые наборы программ. Во время изменения SSH-порта не закрывайте текущую сессию — сначала проверьте новый вход во втором терминале. -Если нужен только мастер без скачивания полного архива: - -```bash -sudo apt-get update -sudo apt-get install -y curl ca-certificates -curl -4 -fLO \ - --retry 5 \ - --retry-delay 10 \ - --connect-timeout 30 \ - https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-install.sh -chmod 700 ochenstarik-server-install.sh -sudo ./ochenstarik-server-install.sh -``` - -При запуске одного мастера недостающие этапы будут загружены автоматически из основной ветки и проверены командой `bash -n`. +Мастер намеренно требует, чтобы все скрипты этапов уже лежали рядом локально. Используйте полный архив или `git clone`; одиночный файл мастера больше не подкачивает недостающие этапы из изменяемой ветки `main`. ## Что входит в проект @@ -385,12 +371,12 @@ zstd -dc /ПУТЬ/К/АРХИВУ.tar.zst | tar --acls --xattrs --numeric-owner ## Полная установка -Рекомендуемый способ — единый мастер. Он показывает этапы по порядку и перед каждым предлагает установить его, пропустить либо завершить работу. Если остальные файлы находятся рядом, мастер использует их. Если какой-либо этап отсутствует, мастер безопасно загрузит его из основной ветки, проверит `bash -n` и только потом запустит. +Рекомендуемый способ — единый мастер. Он показывает главное меню со всеми этапами, запускает выбранный пункт и после завершения возвращается обратно в меню. Если какой-либо этап отсутствует рядом с мастером, запуск будет остановлен: скачайте полный архив проекта или используйте `git clone`. ```bash -curl -fLO https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main/ochenstarik-server-install.sh -chmod 700 ochenstarik-server-install.sh -bash -n ochenstarik-server-install.sh +cd lightweight-server +chmod 700 ./*.sh +bash -n ./*.sh sudo ./ochenstarik-server-install.sh ``` diff --git a/ochenstarik-server-2.sh b/ochenstarik-server-2.sh index 1c5be54..c307d17 100755 --- a/ochenstarik-server-2.sh +++ b/ochenstarik-server-2.sh @@ -278,13 +278,11 @@ apply_ip_mode_and_firewall() { if [[ "$IP_MODE" == ipv4 ]]; then restore_ipv6_stack_if_managed set_ufw_ipv6_setting yes - ufw reload >/dev/null 2>&1 || true delete_managed_ufw_rules else enable_ipv6_systemwide [[ "$IP_MODE" != ipv6 ]] || verify_ipv6_connectivity set_ufw_ipv6_setting yes - ufw reload >/dev/null 2>&1 || true delete_managed_ufw_rules fi @@ -295,6 +293,7 @@ apply_ip_mode_and_firewall() { allow_rule_for_selected_families "$rule" done ufw --force enable + ufw reload >/dev/null 2>&1 || true [[ ! -L "$IP_FAMILY_CONFIG" ]] \ || die "Отказ от записи через символическую ссылку: $IP_FAMILY_CONFIG" printf 'IP_MODE=%s\n' "$IP_MODE" > "$IP_FAMILY_CONFIG" diff --git a/ochenstarik-server-backup-7.sh b/ochenstarik-server-backup-7.sh old mode 100644 new mode 100755 index 1995a69..f4d1fd5 --- a/ochenstarik-server-backup-7.sh +++ b/ochenstarik-server-backup-7.sh @@ -146,6 +146,35 @@ rotate_backups() { done } +read_backup_config() { + local line key value + BACKUP_ROOT="" + [[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] \ + || die "Не найден безопасный файл настроек: $CONFIG_FILE" + [[ "$(stat -c %u "$CONFIG_FILE")" == 0 ]] \ + || die "Файл настроек должен принадлежать root: $CONFIG_FILE" + [[ "$(stat -c %a "$CONFIG_FILE")" =~ ^[0-7]*[0-5][0-5]$ ]] \ + || die "Файл настроек не должен быть доступен для записи группе или всем: $CONFIG_FILE" + + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line//$'\r'/}" + [[ -n "$line" && "$line" != \#* ]] || continue + [[ "$line" == *=* ]] || die "Некорректная строка в $CONFIG_FILE: $line" + key="${line%%=*}" + value="${line#*=}" + case "$key" in + BACKUP_ROOT) + [[ "$value" =~ ^/[A-Za-z0-9._/-]+$ ]] \ + || die "Некорректный BACKUP_ROOT в $CONFIG_FILE" + BACKUP_ROOT="$value" + ;; + *) + die "Неизвестный ключ в $CONFIG_FILE: $key" + ;; + esac + done < "$CONFIG_FILE" +} + create_backup() { local backup_kind="$1" keep_count="$2" timestamp target_dir local archive temporary checksum checksum_temporary exclude_backup_root @@ -226,12 +255,7 @@ create_backup() { } [[ "$EUID" -eq 0 ]] || die "Запустите команду от имени root" -[[ -f "$CONFIG_FILE" && ! -L "$CONFIG_FILE" ]] \ - || die "Не найден безопасный файл настроек: $CONFIG_FILE" -[[ "$(stat -c %u "$CONFIG_FILE")" == 0 ]] \ - || die "Файл настроек должен принадлежать root: $CONFIG_FILE" -# shellcheck source=/dev/null -source "$CONFIG_FILE" +read_backup_config [[ "${BACKUP_ROOT:-}" == /* && "$BACKUP_ROOT" != / ]] \ || die "Некорректный BACKUP_ROOT в $CONFIG_FILE" mkdir -p -m 700 -- "$BACKUP_ROOT"/{initial,daily,weekly,monthly} @@ -363,7 +387,7 @@ done install -d -o root -g root -m 700 "$CONFIG_DIR" "$BACKUP_ROOT" [[ ! -L "$CONFIG_FILE" ]] || die "Отказ от записи через символическую ссылку: $CONFIG_FILE" -printf 'BACKUP_ROOT=%q\n' "$BACKUP_ROOT" > "$CONFIG_FILE" +printf 'BACKUP_ROOT=%s\n' "$BACKUP_ROOT" > "$CONFIG_FILE" chown root:root "$CONFIG_FILE" chmod 600 "$CONFIG_FILE" diff --git a/ochenstarik-server-install.sh b/ochenstarik-server-install.sh old mode 100644 new mode 100755 index 8727c9b..4229128 --- a/ochenstarik-server-install.sh +++ b/ochenstarik-server-install.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -Eeuo pipefail -readonly REPO_RAW_BASE="https://raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main" readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" declare -a STEP_FILES=( @@ -33,8 +32,11 @@ declare -A MASTER_TEXT=() load_master_text() { MASTER_TEXT=( [intro_title]="Unified lightweight-server installer" - [intro_body]="Steps are shown in order. Each step can be installed, skipped, or used to exit the installer. Keep the current SSH session open while changing the SSH port and test the new login in another terminal." + [intro_body]="Select any installation step from the main menu. After a step finishes, the installer returns to this menu. Keep the current SSH session open while changing the SSH port and test the new login in another terminal." [step]="Step %s of %s: %s" + [main_menu]="Main menu" + [select_step]="Enter a step number to install it, or 0 to exit: " + [exit]="Exit" [install]="Install / run this step" [skip]="Skip and continue to the next step" [finish]="Exit the installer" @@ -56,8 +58,9 @@ load_master_text() { case "$UI_LANG" in ru) MASTER_TEXT+=( [intro_title]="Единый мастер установки lightweight-server" - [intro_body]="Этапы будут показаны по порядку. Каждый можно установить, пропустить или завершить мастер. Не закрывайте текущую SSH-сессию при изменении порта и проверьте новый вход во втором терминале." + [intro_body]="Выберите любой этап установки в главном меню. После завершения этапа мастер вернётся обратно в это меню. Не закрывайте текущую SSH-сессию при изменении порта и проверьте новый вход во втором терминале." [step]="Этап %s из %s: %s" [install]="Установить / запустить этот этап" + [main_menu]="Главное меню" [select_step]="Введите номер этапа для установки или 0 для выхода: " [exit]="Выход" [skip]="Пропустить и перейти к следующему" [finish]="Завершить мастер" [action]="Выберите действие: " [invalid]="Введите 1, 2 или 3" [interrupted]="Ввод прерван" [failed]="Этап завершился с ошибкой." [retry]="Запустить этот этап повторно" @@ -208,31 +211,37 @@ ensure_step_script() { local filename="$1" result_variable="$2" target target="${SCRIPT_DIR}/${filename}" - if [[ -e "$target" ]]; then - [[ -f "$target" && ! -L "$target" ]] \ - || die "Отказ от запуска: $target должен быть обычным файлом" - else - command -v curl >/dev/null 2>&1 \ - || die "Не найден curl: установите его командой apt-get install -y curl ca-certificates" - [[ -d "$SCRIPT_DIR" && ! -L "$SCRIPT_DIR" && -w "$SCRIPT_DIR" ]] \ - || die "Каталог $SCRIPT_DIR недоступен для безопасной загрузки" - - log "Файл ${filename} отсутствует; загружаю его из основного репозитория" - TEMP_FILE="$(mktemp "${SCRIPT_DIR}/.ochenstarik-server-download.XXXXXX")" - chmod 600 "$TEMP_FILE" - curl -fL --retry 5 --retry-delay 5 --connect-timeout 30 \ - --proto '=https' --tlsv1.2 "${REPO_RAW_BASE}/${filename}" -o "$TEMP_FILE" - bash -n "$TEMP_FILE" || die "Загруженный файл ${filename} не прошёл проверку Bash" - chmod 700 "$TEMP_FILE" - mv -- "$TEMP_FILE" "$target" - TEMP_FILE="" - fi + [[ -f "$target" && ! -L "$target" ]] \ + || die "Не найден локальный файл ${filename}. Скачайте полный архив проекта или выполните git clone, затем запустите мастер из каталога со всеми скриптами." chmod 700 "$target" bash -n "$target" || die "Синтаксическая ошибка в $target" printf -v "$result_variable" '%s' "$target" } +choose_main_menu_action() { + local result_variable="$1" answer index + while :; do + printf '\n============================================================\n' + printf '%s\n' "$(master_msg main_menu)" + for index in "${!STEP_FILES[@]}"; do + printf ' %s) %s\n' "$((index + 1))" "${STEP_TITLES[index]}" + done + printf ' 0) %s\n' "$(master_msg exit)" + read -rp "$(master_msg select_step)" answer || die "$(master_msg interrupted)" + answer="${answer:-}" + if [[ "$answer" == 0 ]]; then + printf -v "$result_variable" '%s' 0 + return 0 + fi + if [[ "$answer" =~ ^[0-9]+$ ]] && ((10#$answer >= 1 && 10#$answer <= ${#STEP_FILES[@]})); then + printf -v "$result_variable" '%s' "$((10#$answer))" + return 0 + fi + warn "Введите число от 0 до ${#STEP_FILES[@]}" + done +} + choose_step_action() { local step_number="$1" title="$2" result_variable="$3" answer while :; do @@ -284,21 +293,15 @@ printf '\n%s\n\n%s\n' "$(master_msg intro_title)" "$(master_msg intro_body)" completed=0 skipped=0 -for index in "${!STEP_FILES[@]}"; do - step_number="$((index + 1))" - action="" - choose_step_action "$step_number" "${STEP_TITLES[index]}" action - case "$action" in - 2) - skipped="$((skipped + 1))" - continue - ;; - 3) - print_summary "$completed" "$skipped" - exit 0 - ;; - esac - +while :; do + menu_action="" + choose_main_menu_action menu_action + if [[ "$menu_action" == 0 ]]; then + print_summary "$completed" "$skipped" + exit 0 + fi + step_number="$menu_action" + index="$((step_number - 1))" script_path="" ensure_step_script "${STEP_FILES[index]}" script_path while :; do @@ -319,5 +322,3 @@ for index in "${!STEP_FILES[@]}"; do esac done done - -print_summary "$completed" "$skipped" diff --git a/ochenstarik-server-panel-warp-6.sh b/ochenstarik-server-panel-warp-6.sh old mode 100644 new mode 100755 index b114d20..b78c7ac --- a/ochenstarik-server-panel-warp-6.sh +++ b/ochenstarik-server-panel-warp-6.sh @@ -112,6 +112,26 @@ wait_for_tcp_listener() { die "${description} не открыл TCP-порт ${port}" } +choose_public_access() { + local answer + printf '\nДоступ к панели 3x-ui и подпискам:\n' + printf ' 1) Приватный режим: не открывать порты панели и подписок в UFW (рекомендуется)\n' + printf ' 2) Публичный режим: открыть порты панели и подписок всему Интернету\n' + while :; do + read -rp 'Режим доступа [1]: ' answer + answer="${answer:-1}" + case "$answer" in + 1) XUI_PUBLIC_ACCESS=no; return 0 ;; + 2) + warn "Публичный доступ к панели опасен. Используйте сложный путь, новые учётные данные, TLS и 2FA" + XUI_PUBLIC_ACCESS=yes + return 0 + ;; + *) warn "Введите 1 или 2" ;; + esac + done +} + allow_ufw_port() { local port="$1" description="$2" mode mode="$(read_ip_mode)" @@ -265,6 +285,7 @@ forbidden_common=("22" "80" "443") PANEL_PORT="" SUBSCRIPTION_PORT="" WARP_PORT="" +XUI_PUBLIC_ACCESS=no XUI_WEB_PATH="/" XUI_SUB_PATH="/sub/" choose_port PANEL_PORT "Порт панели 3x-ui" "$DEFAULT_PANEL_PORT" "${forbidden_common[@]}" @@ -272,11 +293,17 @@ choose_port SUBSCRIPTION_PORT "Порт подписок 3x-ui" "$DEFAULT_SUBSCR "${forbidden_common[@]}" "$PANEL_PORT" choose_port WARP_PORT "Локальный proxy-порт WARP" "$DEFAULT_WARP_PORT" \ "${forbidden_common[@]}" "$PANEL_PORT" "$SUBSCRIPTION_PORT" +choose_public_access printf '\nБудут использованы порты:\n' printf ' 3x-ui panel: %s/tcp\n' "$PANEL_PORT" printf ' 3x-ui subscriptions: %s/tcp\n' "$SUBSCRIPTION_PORT" printf ' WARP local proxy: 127.0.0.1:%s/tcp\n\n' "$WARP_PORT" +if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then + printf ' UFW: порты панели и подписок будут открыты публично\n\n' +else + printf ' UFW: порты панели и подписок не будут открыты публично\n\n' +fi export DEBIAN_FRONTEND=noninteractive log "Установка системных зависимостей" @@ -306,13 +333,22 @@ install_cloudflare_warp log "Настройка UFW" allow_ufw_port 80 "HTTP" allow_ufw_port 443 "HTTPS" -allow_ufw_port "$PANEL_PORT" "панель 3x-ui" -allow_ufw_port "$SUBSCRIPTION_PORT" "подписки 3x-ui" +if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then + allow_ufw_port "$PANEL_PORT" "панель 3x-ui" + allow_ufw_port "$SUBSCRIPTION_PORT" "подписки 3x-ui" +else + log "UFW: порты панели и подписок оставлены закрытыми для публичного Интернета" +fi ufw --force enable -for port in 80 443 "$PANEL_PORT" "$SUBSCRIPTION_PORT"; do +for port in 80 443; do verify_ufw_port "$port" done +if [[ "$XUI_PUBLIC_ACCESS" == yes ]]; then + for port in "$PANEL_PORT" "$SUBSCRIPTION_PORT"; do + verify_ufw_port "$port" + done +fi wait_for_tcp_listener "$PANEL_PORT" "Панель 3x-ui" wait_for_tcp_listener "$SUBSCRIPTION_PORT" "Сервис подписок 3x-ui" @@ -323,4 +359,7 @@ printf 'Панель 3x-ui: http(s)://:%s%s\n' "$PANEL_PORT" printf 'Подписки 3x-ui: http(s)://:%s%s\n' "$SUBSCRIPTION_PORT" "$XUI_SUB_PATH" printf 'Локальный WARP proxy: socks5://127.0.0.1:%s\n' "$WARP_PORT" printf '\nПорт WARP не открыт публично в UFW: локальный proxy не требует аутентификации.\n' +if [[ "$XUI_PUBLIC_ACCESS" != yes ]]; then + printf 'Порты панели и подписок не открыты публично. Используйте SSH tunnel, management VPN или повторный запуск с явным публичным режимом.\n' +fi printf 'Настройте TLS, сложный путь панели, новые учётные данные и двухфакторную аутентификацию в 3x-ui.\n' diff --git a/ochenstarik-server-uninstall.sh b/ochenstarik-server-uninstall.sh old mode 100644 new mode 100755 index 07e4b72..bc86219 --- a/ochenstarik-server-uninstall.sh +++ b/ochenstarik-server-uninstall.sh @@ -218,12 +218,29 @@ remove_panel_and_warp() { } read_backup_root() { - local value="/var/backups/ochenstarik-server" + local value="/var/backups/ochenstarik-server" line key parsed_value if [[ -f "$BACKUP_CONFIG" && ! -L "$BACKUP_CONFIG" ]]; then [[ "$(stat -c '%u' "$BACKUP_CONFIG")" == 0 ]] \ || die "Файл $BACKUP_CONFIG должен принадлежать root" - value="$(bash -c 'set -efu; source "$1"; printf "%s" "$BACKUP_ROOT"' \ - _ "$BACKUP_CONFIG")" + [[ "$(stat -c '%a' "$BACKUP_CONFIG")" =~ ^[0-7]*[0-5][0-5]$ ]] \ + || die "Файл $BACKUP_CONFIG не должен быть доступен для записи группе или всем" + parsed_value="" + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line//$'\r'/}" + [[ -n "$line" && "$line" != \#* ]] || continue + [[ "$line" == *=* ]] || die "Некорректная строка в $BACKUP_CONFIG: $line" + key="${line%%=*}" + value="${line#*=}" + case "$key" in + BACKUP_ROOT) + [[ "$value" =~ ^/[A-Za-z0-9._/-]+$ ]] \ + || die "Некорректный BACKUP_ROOT в $BACKUP_CONFIG" + parsed_value="$value" + ;; + *) die "Неизвестный ключ в $BACKUP_CONFIG: $key" ;; + esac + done < "$BACKUP_CONFIG" + value="${parsed_value:-$value}" fi [[ "$value" == /* && "$value" != / ]] || die "Опасный путь резервных копий: $value" case "$value" in diff --git a/tests/test-master-language.sh b/tests/test-master-language.sh old mode 100644 new mode 100755 index 9f4e551..27ffd52 --- a/tests/test-master-language.sh +++ b/tests/test-master-language.sh @@ -22,5 +22,10 @@ choose_dialog_language <<< '2' >/dev/null [[ "${#STEP_TITLES[@]}" == 8 ]] [[ "${STEP_FILES[7]}" == 'ochenstarik-server-ai-agents-8.sh' ]] [[ "${STEP_TITLES[7]}" == 'AI-агенты на выбор для обычного пользователя' ]] +[[ "$(master_msg main_menu)" == 'Главное меню' ]] +choose_main_menu_action selected_step <<< '8' >/dev/null +[[ "$selected_step" == 8 ]] +choose_main_menu_action selected_step <<< '0' >/dev/null +[[ "$selected_step" == 0 ]] printf 'Master language selection tests passed.\n' diff --git a/tests/test-security-invariants.sh b/tests/test-security-invariants.sh new file mode 100755 index 0000000..e681e2d --- /dev/null +++ b/tests/test-security-invariants.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." + +if grep -q 'REPO_RAW_BASE' ochenstarik-server-install.sh || + grep -q 'raw.githubusercontent.com/ochenstarik-ui/lightweight-server/main' ochenstarik-server-install.sh; then + printf '[x] Master installer must not fetch missing modules from mutable main\n' >&2 + exit 1 +fi + +if grep -q 'source "$CONFIG_FILE"' ochenstarik-server-backup-7.sh; then + printf '[x] Backup runner must parse config keys instead of sourcing root config\n' >&2 + exit 1 +fi + +if grep -Eq 'bash -c .*source|source "\$1"' ochenstarik-server-uninstall.sh; then + printf '[x] Uninstaller must parse backup config keys instead of sourcing root config\n' >&2 + exit 1 +fi + +if ! grep -q 'XUI_PUBLIC_ACCESS=no' ochenstarik-server-panel-warp-6.sh; then + printf '[x] 3x-ui panel must be private by default\n' >&2 + exit 1 +fi + +if ! grep -q 'Приватный режим' ochenstarik-server-panel-warp-6.sh; then + printf '[x] Step 6 must offer a private access mode for 3x-ui\n' >&2 + exit 1 +fi + +printf '[+] Security invariants look correct\n' diff --git a/tests/test-step2-ipv4-firewall.sh b/tests/test-step2-ipv4-firewall.sh index 56f8016..3ddf82b 100755 --- a/tests/test-step2-ipv4-firewall.sh +++ b/tests/test-step2-ipv4-firewall.sh @@ -41,4 +41,9 @@ if [[ "$ipv4_block" != *'delete_managed_ufw_rules'* ]]; then exit 1 fi +if [[ "$ipv4_block" == *'ufw reload'* ]]; then + printf '[x] IPv4 mode must not reload UFW before stale managed rules are deleted\n' >&2 + exit 1 +fi + printf '[+] Step 2 IPv4 firewall behavior looks correct\n' -- 2.45.2