--restore: значения в файлах настроек копии – по формату каждого поля
Имена в kit.env, kit-backup.env и install-result.env проверялись по белому
списку, а значения – нет: MTPROTO_INNER из чужой копии попадал в выражение
sed в kit (флаг e – выполнение команды). Теперь у каждого имени свой формат
(порты – цифры, пути – буквы и цифры, хосты, флаги – допустимые слова),
кавычки – только пустые (''), неверное значение – копия отклоняется.
kit: sed с портом MTProto – только если порт число. Тесты на все поля.
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user