Безопасность: код установки ретранслятора (M1), zip-slip 7z (M3), L1/L5/L2
Продолжение исправлений по security-аудиту. M1 (MEDIUM) — установщик PHP-ретранслятора был открыт без входа: до создания админа атакующий из интернета мог завести админа кабинета или подменить адрес ядра/ключ. Добавлен «код установки»: генерируется в файл install.lock рядом с данными (вне веб-корня), прочитать его можно только с доступом к файловой системе (FTP/файловый менеджер) — то есть владельцу сервера. Шаги connect и admin (включая «пропустить») требуют верный код. Регексп loopback заякорен (`http://127.0.0.1.attacker.com` больше не проходит как loopback). Код удаляется по завершении установки. Проверено встроенным php-сервером. M3 (MEDIUM) — распаковка 7z в мастере ключа (ExtractISO) сохраняла пути из архива → запись «../…» вышла бы за каталог. Перед распаковкой архив просматривается (7z l -slt, без записи); при абсолютном пути или выходе за корень распаковка отклоняется. Тест TestUnsafeArchivePath. L1 — target_device не проверялся перед root mount: значение не из списка съёмных теперь отклоняется (иначе root смонтировал бы произвольный девайс). L5 — likeEscape в ручном поиске DEPO (manualDepoSearch): метасимволы LIKE от пользователя больше не «расползаются» по базе. L2a — кука сессии ядра получает Secure и за TLS-терминирующим прокси на loopback (reqIsHTTPS учитывает X-Forwarded-Proto только от loopback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -50,6 +50,41 @@ function dataInsideWebroot(): bool
|
||||
return str_starts_with(dataDir(), __DIR__);
|
||||
}
|
||||
|
||||
// installTokenFile — файл одноразового «кода установки». Установщик кабинета
|
||||
// открыт без входа (его ставят в браузере), поэтому чувствительные шаги (сохранить
|
||||
// связь с ядром, завести администратора) требуют код, который лежит в файле рядом
|
||||
// с данными: прочитать его можно только имея доступ к файловой системе (FTP,
|
||||
// файловый менеджер, SSH) — то есть будучи владельцем сервера. Атакующий с одним
|
||||
// HTTP-доступом код не узнает и настройку/создание админа не выполнит.
|
||||
function installTokenFile(): string
|
||||
{
|
||||
return dataDir() . '/install.lock';
|
||||
}
|
||||
|
||||
// installToken — текущий код установки; создаётся при первом показе установщика.
|
||||
function installToken(): string
|
||||
{
|
||||
$f = installTokenFile();
|
||||
if (is_file($f)) {
|
||||
return trim((string)@file_get_contents($f));
|
||||
}
|
||||
try {
|
||||
$tok = bin2hex(random_bytes(16));
|
||||
} catch (Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
@file_put_contents($f, $tok);
|
||||
@chmod($f, 0600);
|
||||
return $tok;
|
||||
}
|
||||
|
||||
// installTokenOK — введённый код совпадает с файлом (постоянное сравнение).
|
||||
function installTokenOK(): bool
|
||||
{
|
||||
$want = installToken();
|
||||
return $want !== '' && hash_equals($want, trim((string)($_POST['itoken'] ?? '')));
|
||||
}
|
||||
|
||||
/** Настройки: файл config.php — PHP-заглушка + JSON, чтобы даже отданный веб-сервером файл ничего не показал. */
|
||||
function loadConfig(): array
|
||||
{
|
||||
@@ -505,12 +540,19 @@ function installer(array $cfg): never
|
||||
page('Кабинет уже настроен', '<div class="box"><p class="mut">Установщик закрыт: настройки сохранены в ' . h(dataDir()) . '/config.php. Чтобы изменить их, удалите этот файл и откройте страницу снова.</p><a class="btn" href="/login">Перейти ко входу</a></div>', $side);
|
||||
}
|
||||
$csrf = csrfToken();
|
||||
installToken(); // создаём файл кода при первом показе, чтобы оператор мог его прочитать
|
||||
$error = '';
|
||||
$notice = '';
|
||||
$vals = ['title' => $cfg['title'] ?? '', 'mode' => $cfg['mode'] ?? 'direct', 'bj_url' => $cfg['bj_url'] ?? '', 'bj_key' => $cfg['bj_key'] ?? ''];
|
||||
|
||||
if ($method === 'POST' && !csrfOK()) {
|
||||
$error = 'Форма устарела, обновите страницу.';
|
||||
} elseif ($method === 'POST' && ($step === 'connect' || $step === 'admin') && !installTokenOK()) {
|
||||
// Чувствительный шаг без верного кода установки — не выполняем.
|
||||
$error = 'Неверный код установки. Откройте файл ' . h(installTokenFile()) . ' через файловый менеджер или FTP и вставьте код из него.';
|
||||
if ($step === 'admin') {
|
||||
adminForm($csrf, '', $error, [['Проверка сервера', true], ['Связь с ядром', true], ['Администратор кабинета', true]], $side);
|
||||
}
|
||||
} elseif ($method === 'POST' && $step === 'connect') {
|
||||
foreach ($vals as $k => $_) {
|
||||
$vals[$k] = trim((string)($_POST[$k] ?? ''));
|
||||
@@ -521,7 +563,7 @@ function installer(array $cfg): never
|
||||
$error = 'Укажите ключ доступа модуля — он создаётся в настройках ядра, раздел «Кабинет клиента».';
|
||||
} elseif ($new['mode'] === 'direct' && $new['bj_url'] === '') {
|
||||
$error = 'Укажите адрес ядра или выберите режим «ядро подключается к кабинету».';
|
||||
} elseif ($new['mode'] === 'direct' && !str_starts_with($new['bj_url'], 'https://') && !preg_match('~^http://(127\.0\.0\.1|localhost)~', $new['bj_url'])) {
|
||||
} elseif ($new['mode'] === 'direct' && !str_starts_with($new['bj_url'], 'https://') && !preg_match('~^http://(127\.0\.0\.1|localhost)(:\d+)?(/|$)~', $new['bj_url'])) {
|
||||
$error = 'Адрес ядра должен начинаться с https:// — по этому каналу идут ключ и сессии клиентов.';
|
||||
} elseif ($new['mode'] === 'agent' && !extension_loaded('pdo_sqlite')) {
|
||||
$error = 'Для режима «ядро подключается к кабинету» нужно расширение sqlite — включите его в панели хостинга или выберите прямой режим.';
|
||||
@@ -590,13 +632,14 @@ function installer(array $cfg): never
|
||||
. '<label class="radio"><input type="radio" name="mode" value="agent"' . ($vals['mode'] === 'agent' ? ' checked' : '') . '> <span><b>Ядро подключается к кабинету</b> — у ядра нет адреса в интернете; оно само дозвонится сюда.</span></label></div>'
|
||||
. '<div><label class="l">Адрес ядра (только для первого способа)</label><input type="text" name="bj_url" value="' . h($vals['bj_url']) . '" placeholder="https://bj.депозитарий.ru" autocomplete="off"></div>'
|
||||
. '<div><label class="l">Ключ доступа модуля</label><input type="password" name="bj_key" value="' . h($vals['bj_key']) . '" autocomplete="off" placeholder="lk-…"><p class="mut" style="margin-top:6px">Создаётся в настройках ядра: «Кабинет клиента» → «Создать ключ».</p></div>'
|
||||
. '<div><label class="l">Код установки</label><input type="text" name="itoken" autocomplete="off" placeholder="из файла install.lock"><p class="mut" style="margin-top:6px">Откройте файл <code>' . h(installTokenFile()) . '</code> через файловый менеджер хостинга или FTP и вставьте код из него — так мы убеждаемся, что настройку выполняет владелец сервера, а не посторонний из интернета.</p></div>'
|
||||
. '<button class="btn" type="submit">Проверить связь и сохранить</button></form>';
|
||||
page('Три шага — и кабинет работает', $f, $side, $steps);
|
||||
}
|
||||
|
||||
function adminForm(string $csrf, string $notice, string $error, array $steps, string $side): never
|
||||
{
|
||||
$f = '<form method="post" action="/install"><input type="hidden" name="csrf" value="' . h($csrf) . '"><input type="hidden" name="step" value="admin">'
|
||||
$f = '<form method="post" action="/install"><input type="hidden" name="csrf" value="' . h($csrf) . '"><input type="hidden" name="step" value="admin"><input type="hidden" name="itoken" value="' . h(installToken()) . '">'
|
||||
. ($error !== '' ? '<div class="msg err">' . h($error) . '</div>' : '') . ($notice !== '' ? '<div class="msg ok">' . h($notice) . '</div>' : '')
|
||||
. '<h2>Администратор кабинета</h2><p class="mut">Сотрудник депозитария, который входит в кабинет, видит список клиентов и может открыть кабинет клиента на просмотр. Учётная запись заводится в ядре. Если администратор уже есть — шаг можно пропустить.</p>'
|
||||
. '<div><label class="l">Логин</label><input type="text" name="login" autocomplete="off"></div>'
|
||||
@@ -610,6 +653,7 @@ function adminForm(string $csrf, string $notice, string $error, array $steps, st
|
||||
|
||||
function done(string $side): never
|
||||
{
|
||||
@unlink(installTokenFile()); // установка завершена — код больше не нужен
|
||||
page('Кабинет установлен', '<div class="box"><h2>Готово</h2><p class="mut">Клиенты входят по логину и паролю, которые выдаёт депозитарий. Учётные записи заводятся в настройках ядра, раздел «Кабинет клиента». Настройки ретранслятора — в ' . h(dataDir()) . '/config.php; чтобы пройти установку заново, удалите его.</p><a class="btn" href="/login">Перейти ко входу</a></div>', $side);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user