Безопасность: fail-closed auth (H1), доверенный прокси для IP (H2), CSRF на /order (M2)
По результатам security-аудита (docs, run-1). Три верифицированные находки. H1 (HIGH) — fail-open аутентификация. loadUsers() при ошибке чтения/парсинга users.json молча возвращал пустой стор → authOpen() считал это первичной настройкой → после удаления first-run файла весь /admin отдавался без входа. Теперь: постоянный маркер configured (пишется при создании первого админа и бэкфиллится при загрузке валидного стора); authOpen() открыт только на реально свежей системе; при пустом/битом сторе на настроенной системе — 503 и блокировка до восстановления файла (authLockedOut). Тест TestAuthFailClosed (4 сценария). H2 (HIGH) — интернет-слой доверял клиентскому X-Forwarded-For/X-Real-IP, что позволяло обойти лимит входа по IP и травить аудит. Теперь заголовкам доверяем только от доверенного прокси (по умолчанию loopback; расширяется BJ_TRUSTED_PROXIES), из XFF берём правый элемент. Исправлено и в Go-модуле (modules/lk/security.go), и в PHP-ретрансляторе. Тесты TestClientIPTrust (Go) и прогон логики PHP. M2 (MEDIUM) — POST /order (поручение на списание бумаг) не проверял CSRF, в отличие от /login и /impersonate. Добавлен csrfOK (форма уже несла токен). Тест TestOrderCSRF. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
@@ -97,14 +97,46 @@ function isLocal(): bool
|
||||
return $h === 'localhost' || $h === '127.0.0.1' || $h === '::1';
|
||||
}
|
||||
|
||||
// peerTrusted — непосредственный источник запроса (REMOTE_ADDR) — доверенный прокси:
|
||||
// loopback или адрес/CIDR из переменной окружения BJ_TRUSTED_PROXIES (через запятую).
|
||||
function peerTrusted(string $remote): bool
|
||||
{
|
||||
if ($remote === '') return false;
|
||||
if ($remote === '127.0.0.1' || $remote === '::1' || str_starts_with($remote, '127.')) return true;
|
||||
$list = getenv('BJ_TRUSTED_PROXIES') ?: '';
|
||||
foreach (explode(',', $list) as $cidr) {
|
||||
$cidr = trim($cidr);
|
||||
if ($cidr === '') continue;
|
||||
if (!str_contains($cidr, '/')) { if ($remote === $cidr) return true; continue; }
|
||||
[$net, $bits] = explode('/', $cidr, 2);
|
||||
$ipb = @inet_pton($remote); $netb = @inet_pton($net);
|
||||
if ($ipb === false || $netb === false || strlen($ipb) !== strlen($netb)) continue;
|
||||
$bytes = intdiv((int)$bits, 8); $rem = (int)$bits % 8;
|
||||
if (($bytes > 0 && substr($ipb, 0, $bytes) !== substr($netb, 0, $bytes))) continue;
|
||||
if ($rem === 0) return true;
|
||||
$mask = chr((0xff << (8 - $rem)) & 0xff);
|
||||
if ((ord($ipb[$bytes]) & ord($mask)) === (ord($netb[$bytes]) & ord($mask))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// clientIP — адрес посетителя. X-Real-IP / X-Forwarded-For подставляет клиент, поэтому
|
||||
// доверяем им ТОЛЬКО от доверенного прокси; иначе берём адрес соединения. Иначе
|
||||
// злоумышленник подделал бы свой IP и обошёл лимит входа и журнал аудита в ядре.
|
||||
function clientIP(): string
|
||||
{
|
||||
foreach (['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $k) {
|
||||
if (!empty($_SERVER[$k])) {
|
||||
return trim(explode(',', $_SERVER[$k])[0]);
|
||||
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
if (peerTrusted($remote)) {
|
||||
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
|
||||
return trim($_SERVER['HTTP_X_REAL_IP']);
|
||||
}
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
// правый элемент добавил наш доверенный прокси; левые может подставить клиент
|
||||
$parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($parts[count($parts) - 1]);
|
||||
}
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
return $remote;
|
||||
}
|
||||
|
||||
function keyOK(string $key): bool
|
||||
|
||||
Reference in New Issue
Block a user