$server `$_SERVER` */ public static function denialReason(bool $canEditModules, array $server): ?string { if (!$canEditModules) { return self::DENIED_RIGHTS; } if (self::header($server, 'REQUEST_METHOD') !== 'POST') { return self::DENIED_METHOD; } return self::isSameOrigin($server) ? null : self::DENIED_ORIGIN; } /** * Origin (браузер ставит его сам, страница чужого сайта подменить не может), а если его нет — * Referer, должен указывать на тот же хост, на который пришёл запрос. Без обоих — отказ: * так же ядро не пускает без Referer восстановление пароля в админку. * * Хост запроса — `Host` и, за обратным прокси, `X-Forwarded-Host`: подставить свой заголовок * в межсайтовый запрос браузер странице не даст. * * @param array $server */ public static function isSameOrigin(array $server): bool { $origin = self::header($server, 'HTTP_ORIGIN'); $source = ($origin !== '') ? $origin : self::header($server, 'HTTP_REFERER'); $sourceHost = self::urlHost($source); if ($sourceHost === null) { return false; } $requestHosts = explode(',', self::header($server, 'HTTP_X_FORWARDED_HOST')); $requestHosts[] = self::header($server, 'HTTP_HOST'); foreach ($requestHosts as $requestHost) { $requestHost = trim($requestHost); if ($requestHost !== '' && self::normalizeHost($requestHost) === $sourceHost) { return true; } } return false; } /** * Хост и порт из абсолютного адреса http(s) или null. Origin `null` (песочница, data:) — null. */ private static function urlHost(string $url): ?string { $parts = parse_url($url); if (!is_array($parts) || !isset($parts['scheme'], $parts['host'])) { return null; } if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) { return null; } return self::normalizeHost($parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '')); } /** * Нижний регистр, без стандартных портов: браузер не пишет `:443` в Origin, а `Host` за * прокси иногда пишет. */ private static function normalizeHost(string $host): string { return (string) preg_replace('/:(80|443)\z/', '', strtolower($host)); } /** * @param array $server */ private static function header(array $server, string $name): string { $value = $server[$name] ?? ''; return is_string($value) ? trim($value) : ''; } }