--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
+57
View File
@@ -43,6 +43,32 @@ def check(p):
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):
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)
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():
d = tempfile.mkdtemp()
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",
}
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=""):
d = tempfile.mkdtemp()
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
+ '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)
@@ -39,6 +55,39 @@ class SafeEnv(unittest.TestCase):
self.assertEqual(code, 0, 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):
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')
@@ -54,7 +103,7 @@ class SafeEnv(unittest.TestCase):
code, out = run(name, text, 'echo "RESULT=$RESULT"\n')
self.assertNotEqual(code, 0, out)
self.assertIn("Это не резервная копия 3X-UI KIT", out)
self.assertNotIn("OK", out)
self.assertNotIn("\nOK\n", "\n" + out)
def test_result_in_backup_env(self):
self.rejected("kit-backup.env", GOOD["kit-backup.env"] + "RESULT=/etc/cron.d/x\n")