110 lines
4.0 KiB
PHP
110 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MonetaPhpShop;
|
|
|
|
/**
|
|
* Ìîæíî ëè ñîõðàíèòü íàñòðîéêè ìîäóëÿ èç ýòîãî çàïðîñà.
|
|
*
|
|
* ßäðî PHPShop ïðîâåðÿåò ïðàâà, òîëüêî åñëè èìÿ äåéñòâèÿ â `actionList[...]` ñîäåðæèò ñóôôèêñ
|
|
* `.modules.edit`, à èìÿ ïðèñûëàåò êëèåíò: `actionUpdate` áåç ñóôôèêñà ÿäðî âûçûâàåò ëþáîìó
|
|
* âîøåäøåìó â àäìèíêó (`PHPShopGUI::getAction()`, ïðîâåðåíî íà ñòåíäå 7.0). Òîêåíà ïðîòèâ CSRF
|
|
* ó ÿäðà íåò, cookie ñåññèè íà ñòåíäå — áåç `SameSite`. Ïîýòîìó ìîäóëü ñàì ïðîâåðÿåò ïðàâî
|
|
* «Ìîäóëè: ðåäàêòèðîâàíèå» è òî, ÷òî çàïðîñ îòïðàâëåí ñî ñòðàíèöû ýòîãî æå ìàãàçèíà.
|
|
*/
|
|
final class SettingsWriteGuard
|
|
{
|
|
public const DENIED_RIGHTS = 'rights';
|
|
public const DENIED_METHOD = 'method';
|
|
public const DENIED_ORIGIN = 'origin';
|
|
|
|
/**
|
|
* Ïðè÷èíà îòêàçà èëè null, åñëè ñîõðàíÿòü ìîæíî.
|
|
*
|
|
* Òîëüêî POST: ÿäðî áåð¸ò `actionList` èç `$_REQUEST`, è áåç ýòîé ïðîâåðêè íàñòðîéêè
|
|
* ñîõðàíÿë áû îáû÷íûé ïåðåõîä ïî ññûëêå. Ó GET áðàóçåð íå øë¸ò Origin, à Referer ñî ñòðàíèöû
|
|
* òîãî æå ñàéòà ïðîâåðêó èñòî÷íèêà ïðîø¸ë áû — ññûëêà íà âèòðèíå ñáðîñèëà áû íàñòðîéêè.
|
|
*
|
|
* @param bool $canEditModules ïðàâî «Ìîäóëè: ðåäàêòèðîâàíèå» ó àäìèíèñòðàòîðà
|
|
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $server
|
|
*/
|
|
private static function header(array $server, string $name): string
|
|
{
|
|
$value = $server[$name] ?? '';
|
|
|
|
return is_string($value) ? trim($value) : '';
|
|
}
|
|
}
|