--restore: значения в файлах настроек копии – по формату каждого поля

Имена в kit.env, kit-backup.env и install-result.env проверялись по белому
списку, а значения – нет: MTPROTO_INNER из чужой копии попадал в выражение
sed в kit (флаг e – выполнение команды). Теперь у каждого имени свой формат
(порты – цифры, пути – буквы и цифры, хосты, флаги – допустимые слова),
кавычки – только пустые (''), неверное значение – копия отклоняется.
kit: sed с портом MTProto – только если порт число. Тесты на все поля.
This commit is contained in:
maxdmitrov
2026-10-06 18:36:48 +03:00
parent 5449a73fd8
commit 397804faa7
5 changed files with 137 additions and 7 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
* `kit check --deep` проверяет подключение к серверу, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало. * `kit check --deep` проверяет подключение к серверу, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало.
* Ссылка подписки в браузере открывается страницей с трафиком, сроком и ссылками (а не белым экраном). Рядом с подпиской теперь ссылки «открыть в приложении»: одно нажатие, и подписка добавлена с названием. Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо. * Ссылка подписки в браузере открывается страницей с трафиком, сроком и ссылками (а не белым экраном). Рядом с подпиской теперь ссылки «открыть в приложении»: одно нажатие, и подписка добавлена с названием. Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо.
* Раздельная маршрутизация: `kit net split on` – и через VPN идёт только список популярных иностранных сервисов (нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской, домашним ничего настраивать не нужно: приложения на Mihomo (Clash Verge, FlClash, Mihomo Party) берут их из подписки, Happ – заголовком, приложения на JSON-подписке Xray – из конфига. DNS не течёт, проверено клиентами Mihomo и Xray. Список в `/etc/kit-sub/rules.yaml`, проверка по базам категорий `kit net split check`, после правки `kit net split apply`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У v2rayN, Karing, Hiddify и приложений с обычным списком ссылок подписка прежняя. * Раздельная маршрутизация: `kit net split on` – и через VPN идёт только список популярных иностранных сервисов (нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской, домашним ничего настраивать не нужно: приложения на Mihomo (Clash Verge, FlClash, Mihomo Party) берут их из подписки, Happ – заголовком, приложения на JSON-подписке Xray – из конфига. DNS не течёт, проверено клиентами Mihomo и Xray. Список в `/etc/kit-sub/rules.yaml`, проверка по базам категорий `kit net split check`, после правки `kit net split apply`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У v2rayN, Karing, Hiddify и приложений с обычным списком ссылок подписка прежняя.
* Исправил `--restore` на сервер с другим IP: ссылки части протоколов (адреса в таблице `hosts` панели и в настройках kit-sub) оставались со старым IP, а проверка панели после запуска могла ложно сообщить «Панель не отвечает», если панель стартует не сразу. А файлы настроек из копии `--restore` теперь читает строго по списку, как охранник на входе: лишнее имя – и копия не принимается, ничего не меняется. * Исправил `--restore` на сервер с другим IP: ссылки части протоколов (адреса в таблице `hosts` панели и в настройках kit-sub) оставались со старым IP, а проверка панели после запуска могла ложно сообщить «Панель не отвечает», если панель стартует не сразу. А файлы настроек из копии `--restore` теперь читает строго по списку, как охранник на входе: лишнее имя – и копия не принимается, ничего не меняется. Охранник теперь смотрит и в лицо: каждое значение проверяется по своему формату (порт – цифры, путь – буквы и цифры, адрес – адрес), а странное – повод не пустить копию, а не молча что-то поправить.
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если токен API панели в `/etc/x-ui/install-result.env` не подошёл (так бывает после обновления с 1.0), `kit` сам выпускает новый и сохраняет его, а если панель всё же не отвечает – говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response». * Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если токен API панели в `/etc/x-ui/install-result.env` не подошёл (так бывает после обновления с 1.0), `kit` сам выпускает новый и сохраняет его, а если панель всё же не отвечает – говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
* Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед. * Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед.
* `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела, а на бессмыслицу вместо запроса – честным `400 Bad Request` со строкой статуса. Ещё он говорит `Connection: keep-alive` и соглашается на `http/1.1` при знакомстве по TLS. Как настоящий nginx, а не его двоюродный брат. * `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела, а на бессмыслицу вместо запроса – честным `400 Bad Request` со строкой статуса. Ещё он говорит `Connection: keep-alive` и соглашается на `http/1.1` при знакомстве по TLS. Как настоящий nginx, а не его двоюродный брат.
+25 -3
View File
@@ -1697,13 +1697,36 @@ sub_links() {
# Файл настроек из копии можно читать, только если в нём нет ничего, кроме # Файл настроек из копии можно читать, только если в нём нет ничего, кроме
# ИМЯ=значение без подстановок и команд, и все имена – из списка для этого файла. # ИМЯ=значение без подстановок и команд, и все имена – из списка для этого файла.
# Иначе чужой архив подменил бы переменные установщика или kit (RESULT, PATH, KIT_VERSION…). # Иначе чужой архив подменил бы переменные установщика или kit (RESULT, PATH, KIT_VERSION…).
# Значение каждого имени – тоже по его формату: kit потом подставляет их в sed, nginx и ссылки.
# Кавычки – только пустые (''): так printf %q пишет пустое значение, остальное у нас без кавычек.
safe_env() { # файл имя... safe_env() { # файл имя...
local f=$1 n; shift local f=$1 n re; shift
[[ -r $f ]] || die "В копии нет файла ${f##*/}." [[ -r $f ]] || die "В копии нет файла ${f##*/}."
grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|'[^']*'))?$" "$f" \ grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|''))?$" "$f" \
&& die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял." && die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."
while IFS= read -r n; do while IFS= read -r n; do
[[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял." [[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."
case $n in
BACKUP_KIT_VERSION) re='^[0-9]{1,3}(\.[0-9]{1,3}){1,3}$' ;;
BACKUP_HOST | HOST) re='^[A-Za-z0-9.:-]{1,253}$' ;;
LINK_HOST) re='^[A-Za-z0-9.:-]{0,253}$' ;;
BACKUP_SSL) re='^(ip|custom|none)$' ;;
BACKUP_DATE) re='^[0-9]{4}-[0-9]{2}-[0-9]{2}$' ;;
XUI_USERNAME | XUI_PASSWORD) re='^[A-Za-z0-9._:/@%+,=-]{1,128}$' ;;
XUI_PANEL_PORT | SUB_INTERNAL) re='^[0-9]{1,5}$' ;;
XUI_WEB_BASE_PATH) re='^/?[A-Za-z0-9_-]{1,64}/?$' ;;
XUI_ACCESS_URL) re='^(https?://[A-Za-z0-9._:-]{1,253}/[A-Za-z0-9_/-]{0,130})?$' ;;
XUI_API_TOKEN) re='^[A-Za-z0-9._+/=-]{0,200}$' ;;
XUI_DB_TYPE) re='^(sqlite|postgres)?$' ;;
PANEL_ON) re='^(ip|domain)?$' ;;
SUB_BASE) re='^https?://[A-Za-z0-9.:-]{1,253}(/[A-Za-z0-9_-]{1,64}){0,4}/?$' ;;
SUB_PATH) re='^/[A-Za-z0-9_-]{1,64}/$' ;;
SINGLE) re='^(yes|no)?$' ;;
MTPROTO_INNER) re='^[0-9]{0,5}$' ;;
*) die "Это не резервная копия 3X-UI KIT или она повреждена (имя $n в ${f##*/}). Ничего не менял." ;;
esac
[[ $(env_get "$f" "$n") =~ $re ]] \
|| die "Это не резервная копия 3X-UI KIT или она повреждена (значение $n в ${f##*/}). Ничего не менял."
done < <(grep -oE '^[A-Z][A-Z0-9_]*' "$f") done < <(grep -oE '^[A-Z][A-Z0-9_]*' "$f")
return 0 return 0
} }
@@ -1929,7 +1952,6 @@ PY
SINGLE=$(env_get "$tmp/etc/kit/kit.env" SINGLE); SINGLE=${SINGLE:-no} SINGLE=$(env_get "$tmp/etc/kit/kit.env" SINGLE); SINGLE=${SINGLE:-no}
SUB_PATH=$(env_get "$tmp/etc/kit/kit.env" SUB_PATH) SUB_PATH=$(env_get "$tmp/etc/kit/kit.env" SUB_PATH)
LINK_HOST=$(env_get "$tmp/etc/kit/kit.env" LINK_HOST) LINK_HOST=$(env_get "$tmp/etc/kit/kit.env" LINK_HOST)
[[ $LINK_HOST =~ ^[A-Za-z0-9.:-]*$ ]] || LINK_HOST=""
SUB_INTERNAL=$(env_get "$tmp/etc/kit/kit.env" SUB_INTERNAL) SUB_INTERNAL=$(env_get "$tmp/etc/kit/kit.env" SUB_INTERNAL)
# kit потом подключает этот файл от root: адрес в нём должен быть тем же, что в копии. # kit потом подключает этот файл от root: адрес в нём должен быть тем же, что в копии.
[[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ \ [[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ \
+3 -1
View File
@@ -158,7 +158,9 @@ collect_links() {
grep -q '://' <<<"$raw" || raw=$(base64 -d <<<"$raw" 2>/dev/null || true) grep -q '://' <<<"$raw" || raw=$(base64 -d <<<"$raw" 2>/dev/null || true)
out+=$(grep -E "$filter" <<<"$raw" || true)$'\n' out+=$(grep -E "$filter" <<<"$raw" || true)$'\n'
done done
[[ ${SINGLE:-no} == yes ]] && out=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=${MTPROTO_INNER:-10445}/\1\2port=443/" <<<"$out") # Порт попадает в выражение sed – только если это число.
local inner=${MTPROTO_INNER:-10445}
[[ ${SINGLE:-no} == yes && $inner =~ ^[0-9]{1,5}$ ]] && out=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=$inner/\1\2port=443/" <<<"$out")
grep . <<<"$out" || true grep . <<<"$out" || true
} }
+57
View File
@@ -43,6 +43,32 @@ def check(p):
PEM = b"-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n" PEM = b"-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n"
# Проверка файлов настроек – те же строки, что в --restore, после распаковки архива.
FUNCS = "".join(re.search(r"^%s\(\) \{.*?^\}\n" % n, src, re.S | re.M).group(0) for n in ("safe_env", "env_get"))
ENV_CHECK = src[src.index(' safe_env "$tmp/kit-backup.env"'):src.index(' python3 -c "import sqlite3')]
ENVS = {
"kit-backup.env": b"BACKUP_KIT_VERSION=1.2\nBACKUP_HOST=1.2.3.4\nBACKUP_SSL=ip\nBACKUP_DATE=2026-10-06\n",
"etc/x-ui/install-result.env": b"XUI_USERNAME=abcDEF1234\nXUI_PASSWORD=Pass1234567890abcdef\nXUI_PANEL_PORT=31234\n"
b"XUI_WEB_BASE_PATH=AbCdEf123456789012\nXUI_ACCESS_URL=https://1.2.3.4:31234/AbCdEf123456789012\n"
b"XUI_API_TOKEN=tok_ABCdef0123456789\nXUI_DB_TYPE=sqlite\n",
"etc/kit/kit.env": b"HOST=1.2.3.4\nLINK_HOST=1.2.3.4\nPANEL_ON=ip\nSUB_BASE=https://1.2.3.4/AbC123/\nSUB_PATH=/AbC123/\n"
b"SUB_INTERNAL=10446\nSINGLE=yes\nMTPROTO_INNER=10445\n",
}
# kit.env 1.1.x: без LINK_HOST и PANEL_ON, подписка на своём порту.
KIT_11 = b"HOST=1.2.3.4\nSUB_BASE=https://1.2.3.4:2096/AbC123/\nSUB_PATH=/AbC123/\nSUB_INTERNAL=2096\nSINGLE=no\nMTPROTO_INNER=''\n"
def restore_env(over=None):
files = dict(ENVS)
files.update(over or {})
p = archive([(k, v) for k, v in files.items()])
out = tempfile.mkdtemp()
r = subprocess.run(["python3", "-", p, out], input=CHECK, capture_output=True, text=True)
if r.returncode:
return r
script = 'set -Eeuo pipefail\ndie() { echo "DIE: $*"; exit 1; }\n' + FUNCS + 'tmp=$1\n' + ENV_CHECK + 'echo ENV-OK\n'
return subprocess.run(["bash", "-c", script, "t", out], capture_output=True, text=True)
class Archive(unittest.TestCase): class Archive(unittest.TestCase):
def test_good(self): def test_good(self):
@@ -74,6 +100,37 @@ class Archive(unittest.TestCase):
self.assertNotEqual(check(archive([("etc/x-ui/../../tmp/x", b"x")])).returncode, 0) self.assertNotEqual(check(archive([("etc/x-ui/../../tmp/x", b"x")])).returncode, 0)
class RestoreEnv(unittest.TestCase):
def test_good_12(self):
r = restore_env()
self.assertIn("ENV-OK", r.stdout, r.stdout + r.stderr)
def test_good_11(self):
r = restore_env({"etc/kit/kit.env": KIT_11,
"kit-backup.env": b"BACKUP_KIT_VERSION=1.1.2\nBACKUP_HOST=1.2.3.4\nBACKUP_SSL=ip\nBACKUP_DATE=2026-09-01\n"})
self.assertIn("ENV-OK", r.stdout, r.stdout + r.stderr)
def bad(self, over):
r = restore_env(over)
self.assertNotIn("ENV-OK", r.stdout)
self.assertIn("Это не резервная копия 3X-UI KIT", r.stdout + r.stderr)
def test_mtproto_inner_sed(self):
for v in (b"MTPROTO_INNER='[0-9]*.*/touch \\/tmp\\/pwn-test/e;#'", b"MTPROTO_INNER='1/x/;1e touch /tmp/pwn-test #'", b"MTPROTO_INNER=1/x/e"):
self.bad({"etc/kit/kit.env": ENVS["etc/kit/kit.env"].replace(b"MTPROTO_INNER=10445", v)})
def test_panel_path_nginx(self):
for v in (b"XUI_WEB_BASE_PATH='x/ { return 200; } location /zz'", b"XUI_WEB_BASE_PATH=x/../y", b"XUI_WEB_BASE_PATH=$(id)"):
self.bad({"etc/x-ui/install-result.env": ENVS["etc/x-ui/install-result.env"].replace(b"XUI_WEB_BASE_PATH=AbCdEf123456789012", v)})
def test_values_every_file(self):
for name in ENVS:
for line in ENVS[name].splitlines():
n = line.split(b"=")[0]
for v in (b"'a;b'", b"$(touch /tmp/pwn-test)", b"a b", b"x" * 300):
self.bad({name: ENVS[name].replace(line, n + b"=" + v)})
def make_db(): def make_db():
d = tempfile.mkdtemp() d = tempfile.mkdtemp()
p = os.path.join(d, "x-ui.db") p = os.path.join(d, "x-ui.db")
+51 -2
View File
@@ -20,12 +20,28 @@ GOOD = {
"SUB_INTERNAL=2096\nSINGLE=yes\nMTPROTO_INNER=''\n", "SUB_INTERNAL=2096\nSINGLE=yes\nMTPROTO_INNER=''\n",
} }
NAMES = {"kit-backup.env": BACKUP, "install-result.env": XUI, "kit.env": KIT} NAMES = {"kit-backup.env": BACKUP, "install-result.env": XUI, "kit.env": KIT}
# Другие настоящие варианты: kit.env 1.1.x (без LINK_HOST и PANEL_ON, свой порт подписки), «всё на 443» с доменом,
# install-result.env с путём панели в слешах (панель стояла до установщика) и пустыми значениями.
GOOD_MORE = [
("kit.env", "HOST=1.2.3.4\nSUB_BASE=https://1.2.3.4:2096/AbC123/\nSUB_PATH=/AbC123/\nSUB_INTERNAL=2096\nSINGLE=no\nMTPROTO_INNER=''\n"),
("kit.env", "HOST=1.2.3.4\nSUB_BASE=http://127.0.0.1:2096/AbC123/\nSUB_PATH=/AbC123/\nSUB_INTERNAL=2096\nSINGLE=no\nMTPROTO_INNER=''\n"),
("kit.env", "HOST=1.2.3.4\nSUB_BASE=https://1.2.3.4/Ab-C_123/\nSUB_PATH=/Ab-C_123/\nSUB_INTERNAL=10446\nSINGLE=yes\nMTPROTO_INNER=10445\n"),
("kit.env", "HOST=vpn.example.com\nLINK_HOST=vpn.example.com\nPANEL_ON=domain\nSUB_BASE=https://vpn.example.com/AbC123/\n"
"SUB_PATH=/AbC123/\nSUB_INTERNAL=10446\nSINGLE=yes\nMTPROTO_INNER=10445\n"),
("install-result.env", "XUI_USERNAME=admin\nXUI_PASSWORD=Pass_1234-abc.def\nXUI_PANEL_PORT=2053\nXUI_WEB_BASE_PATH=/AbCdEf123456789012/\n"
"XUI_ACCESS_URL=http://SERVER_IP_UNKNOWN:2053//AbCdEf123456789012/\nXUI_API_TOKEN=''\nXUI_DB_TYPE=sqlite\n"),
("kit-backup.env", "BACKUP_KIT_VERSION=1.1.2\nBACKUP_HOST=vpn.example.com\nBACKUP_SSL=custom\nBACKUP_DATE=2026-09-01\n"),
]
# Плохие значения: проверяются для каждого имени в каждом файле.
BAD_VALUES = ("$(touch /tmp/pwn-test)", "`id`", "1;id", "1; id", "'1;id'", "'a b'", "a b", "'", "\"x\"", "$HOME", "x\\y",
"1\nid", "'1\n/e;#'", "a" * 300, "9" * 300, "x{", "a|b", "a&b", "a>b", "a*b")
def run(name, text, extra=""): def run(name, text, extra=""):
d = tempfile.mkdtemp() d = tempfile.mkdtemp()
p = os.path.join(d, name) p = os.path.join(d, name)
open(p, "w").write(text) with open(p, "w") as fh:
fh.write(text)
script = ('set -Eeuo pipefail\ndie() { echo "DIE: $*"; exit 1; }\n' + funcs script = ('set -Eeuo pipefail\ndie() { echo "DIE: $*"; exit 1; }\n' + funcs
+ 'RESULT=/root/3x-ui.txt\nsafe_env "$1" ' + " ".join(NAMES[name]) + '\necho OK\n' + extra) + 'RESULT=/root/3x-ui.txt\nsafe_env "$1" ' + " ".join(NAMES[name]) + '\necho OK\n' + extra)
r = subprocess.run(["bash", "-c", script, "t", p], capture_output=True, text=True) r = subprocess.run(["bash", "-c", script, "t", p], capture_output=True, text=True)
@@ -39,6 +55,39 @@ class SafeEnv(unittest.TestCase):
self.assertEqual(code, 0, out) self.assertEqual(code, 0, out)
self.assertIn("OK", out) self.assertIn("OK", out)
def test_good_variants(self):
for name, text in GOOD_MORE:
code, out = run(name, text)
self.assertEqual(code, 0, name + ": " + out)
self.assertIn("OK", out)
def test_bad_value_every_field(self):
for name, text in GOOD.items():
for n in NAMES[name]:
for bad in BAD_VALUES:
lines = [l for l in text.splitlines() if not l.startswith(n + "=")] + [n + "=" + bad]
self.rejected(name, "\n".join(lines) + "\n")
def test_wrong_format(self):
# Безопасные символы, но не тот формат.
for name, n, bad in (("kit.env", "MTPROTO_INNER", "1/x/e"), ("kit.env", "MTPROTO_INNER", "abc"), ("kit.env", "SINGLE", "maybe"),
("kit.env", "PANEL_ON", "x"), ("kit.env", "SUB_PATH", "/../"), ("kit.env", "SUB_PATH", "/a"),
("kit.env", "SUB_BASE", "file:///etc/"), ("kit.env", "SUB_INTERNAL", "20x6"), ("kit.env", "HOST", ""),
("kit.env", "LINK_HOST", "a/b"), ("kit-backup.env", "BACKUP_SSL", "le"), ("kit-backup.env", "BACKUP_DATE", "today"),
("kit-backup.env", "BACKUP_KIT_VERSION", "1.2/x"), ("install-result.env", "XUI_WEB_BASE_PATH", "a/b"),
("install-result.env", "XUI_WEB_BASE_PATH", ""), ("install-result.env", "XUI_WEB_BASE_PATH", "a.b"),
("install-result.env", "XUI_DB_TYPE", "mysql"), ("install-result.env", "XUI_PANEL_PORT", "123456"),
("install-result.env", "XUI_USERNAME", ""), ("install-result.env", "XUI_ACCESS_URL", "ftp://1.2.3.4/x")):
lines = [l for l in GOOD[name].splitlines() if not l.startswith(n + "=")] + [n + "=" + bad]
self.rejected(name, "\n".join(lines) + "\n")
def test_audit_payloads(self):
# Из повторного аудита 1.2: выражение sed с флагом e и чужие директивы nginx.
for bad in ("MTPROTO_INNER='[0-9]*.*/touch \\/tmp\\/pwn-test/e;#'", "MTPROTO_INNER='1/x/;1e touch /tmp/pwn-test #'"):
self.rejected("kit.env", GOOD["kit.env"].replace("MTPROTO_INNER=''\n", bad + "\n"))
self.rejected("install-result.env", GOOD["install-result.env"].replace(
"XUI_WEB_BASE_PATH=AbCdEf123456789012", "XUI_WEB_BASE_PATH='x/ { return 200; } location /zz'"))
def test_values(self): def test_values(self):
code, out = run("kit-backup.env", GOOD["kit-backup.env"], code, out = run("kit-backup.env", GOOD["kit-backup.env"],
'echo "[$(env_get "$1" BACKUP_HOST)|$(env_get "$1" BACKUP_SSL)|$(env_get "$1" NOPE)]"\n') 'echo "[$(env_get "$1" BACKUP_HOST)|$(env_get "$1" BACKUP_SSL)|$(env_get "$1" NOPE)]"\n')
@@ -54,7 +103,7 @@ class SafeEnv(unittest.TestCase):
code, out = run(name, text, 'echo "RESULT=$RESULT"\n') code, out = run(name, text, 'echo "RESULT=$RESULT"\n')
self.assertNotEqual(code, 0, out) self.assertNotEqual(code, 0, out)
self.assertIn("Это не резервная копия 3X-UI KIT", out) self.assertIn("Это не резервная копия 3X-UI KIT", out)
self.assertNotIn("OK", out) self.assertNotIn("\nOK\n", "\n" + out)
def test_result_in_backup_env(self): def test_result_in_backup_env(self):
self.rejected("kit-backup.env", GOOD["kit-backup.env"] + "RESULT=/etc/cron.d/x\n") self.rejected("kit-backup.env", GOOD["kit-backup.env"] + "RESULT=/etc/cron.d/x\n")