Вход Регистрация
Файл: system/security.php
Строк: 449
<?php
/* Обновление игры под PHP 8.4 / PDO и усиление безопасности: by Метриум (Стэлп) */
declare(strict_types=1);

function 
security_client_ip(): string {
    
$ip=$_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    
$cfg=require __DIR__.'/config.php';
    if((
$cfg['security']['trust_proxy'] ?? false) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        
$candidate=trim(explode(',',$_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
        if(
filter_var($candidate,FILTER_VALIDATE_IP)) $ip=$candidate;
    }
    return 
filter_var($ip,FILTER_VALIDATE_IP) ? $ip '0.0.0.0';
}
function 
security_bootstrap(): void {
    if(
PHP_SAPI==='cli') return;
    if((int)(
$_SERVER['CONTENT_LENGTH'] ?? 0) > 2*1024*1024){ http_response_code(413); exit('Слишком большой объём данных.'); }
    if(
strlen((string)($_SERVER['QUERY_STRING'] ?? '')) > 4096){ http_response_code(414); exit('Слишком длинные данные.'); }
    
ini_set('session.use_strict_mode','1');
    
ini_set('session.use_only_cookies','1');
    
ini_set('session.cookie_httponly','1');
    
ini_set('session.cookie_samesite','Lax');
    if(!empty(
$_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off'ini_set('session.cookie_secure','1');
    
header('X-Content-Type-Options: nosniff');
    
header('X-Frame-Options: SAMEORIGIN');
    
header('Referrer-Policy: strict-origin-when-cross-origin');
    
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()');
    if(!empty(
$_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off'header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
    
header('Cross-Origin-Opener-Policy: same-origin');
    
header("Content-Security-Policy: default-src 'self' https: data:; img-src 'self' https: data:; style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline'; connect-src 'self' https:; form-action 'self' https:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'");
}


/* Дополнительный request-firewall без изменения подключения к БД — by Метриум (Стэлп). */
function security_request_guard(): void {
    if (
PHP_SAPI === 'cli') return;
    
$method strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
    if (!
in_array($method, ['GET','HEAD','POST'], true)) {
        
http_response_code(405); header('Allow: GET, HEAD, POST'); exit('Метод не разрешён.');
    }
    
$uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
    if (
str_contains($uri"") || preg_match('/[rn]/'$uri)) { http_response_code(400); exit('Действие не принято.'); }
    foreach ([
$_GET$_POST$_COOKIE] as $bag) {
        if (!
security_input_tree_ok($bag0)) { http_response_code(400); exit('Некорректные входные данные.'); }
    }
    if (
$method === 'POST' && !security_same_origin_request()) {
        
http_response_code(403); exit('Действие не подтверждено. Повторите попытку.');
    }
    
// Legacy-игра содержит GET-действия. Fetch Metadata блокирует их запуск с чужих сайтов,
    // не меняя старые внутренние игровые ссылки.
    
$fetchSite strtolower((string)($_SERVER['HTTP_SEC_FETCH_SITE'] ?? ''));
    if (
$method === 'GET' && security_query_looks_mutating($_GET)) {
        
// Защита старых GET-действий до их полного перевода на POST: внешний Origin/Referer
        // или Fetch Metadata никогда не могут запускать покупку, бой, кик, принятие и т.п.
        
if ($fetchSite === 'cross-site' || !security_same_origin_request()) {
            
http_response_code(403); exit('Это действие нельзя выполнить из внешнего источника.');
        }
    }
}
function 
security_input_tree_ok(mixed $valueint $depth=0): bool {
    if (
$depth 5) return false;
    if (
is_array($value)) {
        if (
count($value) > 150) return false;
        foreach (
$value as $k=>$v) {
            if (
strlen((string)$k) > 128 || !security_input_tree_ok($v$depth+1)) return false;
        }
        return 
true;
    }
    if (
is_string($value)) return strlen($value) <= 65535 && !str_contains($value"");
    return 
true;
}
function 
security_same_origin_request(): bool {
    
$host strtolower(preg_replace('/:d+$/''', (string)($_SERVER['HTTP_HOST'] ?? '')));
    if (
$host === '') return true;
    foreach ([
'HTTP_ORIGIN','HTTP_REFERER'] as $key) {
        
$v=(string)($_SERVER[$key] ?? ''); if($v==='') continue;
        
$h=strtolower((string)(parse_url($vPHP_URL_HOST) ?? ''));
        return 
$h !== '' && hash_equals($host,$h);
    }
    
$fetchSite=strtolower((string)($_SERVER['HTTP_SEC_FETCH_SITE'] ?? ''));
    return !
in_array($fetchSite,['cross-site'],true);
}
function 
security_query_looks_mutating(array $q): bool {
    
$keys=['buy','sell','wear','unwear','use','attack','fight','win','lose','upgrade','up','delete','del','kick','ban','mute','claim','reward','make','startMaking','heal','dodge','exit','go','vugn','stone','grass','kamen','trava','read_all','nread','sread','enter','zay','yes','invite','cancel_invite','clan_msg_read','clan_invite_accept','clan_invite_decline','buy_chest','complete','vizok','vizno','delit_to','stick','close','comment','act','take','join','leave','accept','reject','promote','demote','remove','confirm','cancel','pay','open','craft'];
    foreach(
$keys as $k) if(array_key_exists($k,$q)) return true;
    
$action=strtolower((string)($q['action'] ?? ''));
    return 
in_array($action,['buy','sell','delete','del','kick','ban','mute','upgrade','claim','fight','attack','exit','sent','lose','win','enter','join','leave','accept','reject','promote','demote','remove','confirm','cancel','pay','open','craft','start','finish'],true);
}
function 
security_sql_identifier(string $value, array $allowed=[]): ?string {
    if (
$allowed && !in_array($value,$allowed,true)) return null;
    return 
preg_match('/^[A-Za-z0-9_]+$/D',$value) ? $value null;
}

/**
 * Безопасный рендер старых системных сообщений без зависимости от functions.php.
 * Разрешает только локальные картинки, локальные ссылки, известные span-классы
 * и небольшой белый список BB-кодов. by Метриум (Стэлп)
 */
function security_system_text(mixed $value): string {
    
$text=(string)$value;
    
$safe=[];
    
$put=function(string $html) use (&$safe): string $k='%%SAFE'.count($safe).'%%'$safe[$k]=$html; return $k; };

    
// Локальные картинки старого формата, включая атрибуты без кавычек.
    
$text=preg_replace_callback('~<img\s+[^>]*src\s*=\s*["\']?(/images/[A-Za-z0-9_./-]+)["\']?[^>]*>~i',
        
fn($m)=>$put('<img src="'.security_html($m[1]).'" alt="*">'),$text) ?? $text;
    
// Только внутренние ссылки игры.
    
$text=preg_replace_callback('~<a\s+[^>]*href\s*=\s*["\']?(/[^"\'\s>]+)["\']?[^>]*>(.*?)</a>~is',
        function(
$m) use ($put){
            
$href=(string)$m[1];
            if(
str_starts_with($href,'//') || preg_match('~[\r\n<>]~',$href)) return security_html(strip_tags((string)$m[2]));
            return 
$put('<a href="'.security_html($href).'">'.security_system_text($m[2]).'</a>');
        },
$text) ?? $text;
    
// Известные цветовые классы.
    
$text=preg_replace_callback('~<span\s+class\s*=\s*["\']?([A-Za-z0-9_-]+)["\']?\s*>(.*?)</span>~is',
        function(
$m) use ($put){
            
$allowed=['dgreen','green','red','dred','blue','grey','yellow','orange','purple','white','bold','medium','small'];
            
$cls=strtolower((string)$m[1]);
            if(!
in_array($cls,$allowed,true)) return (string)$m[2];
            return 
$put('<span class="'.$cls.'">'.security_system_text($m[2]).'</span>');
        },
$text) ?? $text;
    foreach([
'br','hr'] as $tag){
        
$text=preg_replace_callback('~<'.$tag.'\s*/?>~i',fn()=> $put('<'.$tag.'>'),$text) ?? $text;
    }
    foreach([
'b','strong','i','em','u'] as $tag){
        
$text=preg_replace_callback('~<'.$tag.'>(.*?)</'.$tag.'>~is',fn($m)=>$put('<'.$tag.'>'.security_system_text($m[1]).'</'.$tag.'>'),$text) ?? $text;
    }

    
$text=security_html($text);
    
// Небольшой безопасный BBCode для системных сообщений.
    
foreach(['b'=>'strong','i'=>'em','u'=>'u','s'=>'s'] as $bb=>$tag){
        
$text=preg_replace('~\['.$bb.'\](.*?)\[/'.$bb.'\]~is','<'.$tag.'>$1</'.$tag.'>',$text) ?? $text;
    }
    
$text=preg_replace_callback('~\[color=([#A-Za-z0-9]{3,16})\](.*?)\[/color\]~is',function($m){
        
$c=(string)$m[1]; if(!preg_match('/^(?:#[0-9a-f]{3,8}|[a-z]{3,16})$/i',$c)) return $m[2];
        return 
'<span style="color:'.security_html($c).'">'.$m[2].'</span>';
    },
$text) ?? $text;
    
$text=preg_replace_callback('~\[img\](/{1}images/[A-Za-z0-9_./-]+)\[/img\]~i',fn($m)=>'<img src="'.security_html($m[1]).'" alt="*">',$text) ?? $text;
    
$text=preg_replace_callback('~\[url=(/[^\]<>\s]+)\](.*?)\[/url\]~is',fn($m)=>'<a href="'.security_html($m[1]).'">'.$m[2].'</a>',$text) ?? $text;
    
$text=nl2br($text,false);
    foreach(
$safe as $k=>$html$text=str_replace($k,$html,$text);
    return 
$text;
}

function 
game_flash_render_pending(): string {
    
$out='';
    foreach([
'err'=>'error','ok'=>'ok','info'=>'info'] as $key=>$cls){
        if(!isset(
$_SESSION[$key]) || $_SESSION[$key]==='') continue;
        
$msg=(string)$_SESSION[$key]; unset($_SESSION[$key]);
        
// Системные сообщения игры могут содержать только ограниченную локальную разметку.
        
$safe=security_system_text($msg);
        
$out.='<div class="game-flash game-flash-'.$cls.'" role="alert">'.$safe.'</div>';
    }
    return 
$out;
}
function 
security_html(mixed $value): string {
    return 
htmlspecialchars((string)$valueENT_QUOTES|ENT_SUBSTITUTE'UTF-8');
}

function 
security_csrf_token(): string {
    if(
session_status()!==PHP_SESSION_ACTIVEsession_start();
    if(empty(
$_SESSION['_csrf'])) $_SESSION['_csrf']=bin2hex(random_bytes(32));
    return 
$_SESSION['_csrf'];
}
function 
security_csrf_field(): string { return '<input type="hidden" name="_csrf" value="'.htmlspecialchars(security_csrf_token(),ENT_QUOTES,'UTF-8').'">'; }
function 
security_require_csrf(): void {
    if((
$_SERVER['REQUEST_METHOD'] ?? 'GET')!=='POST') return;
    
$token=(string)($_POST['_csrf'] ?? '');
    if(!
hash_equals((string)($_SESSION['_csrf'] ?? ''),$token)){ http_response_code(419); exit('Сессия формы устарела. Обновите страницу и повторите действие.'); }
}
function 
security_rate_limit(string $bucket,int $limit,int $window): bool {
    
$dir=sys_get_temp_dir().'/game_rate';
    if(!
is_dir($dir) && !@mkdir($dir,0700,true) && !is_dir($dir)) return true;
    
$now=time();
    
// Периодически удаляем старые rate-limit файлы, чтобы temp-каталог не рос бесконечно.
    
static $cleaned=false;
    if(!
$cleaned){
        
$cleaned=true;
        
$marker=$dir.'/.cleanup';
        
$last=@filemtime($marker) ?: 0;
        if(
$now-$last>21600){
            foreach((array)@
glob($dir.'/*.json') as $oldFile){
                
$mt=@filemtime($oldFile) ?: $now;
                if(
$now-$mt>172800) @unlink($oldFile);
            }
            @
touch($marker);
        }
    }
    
$key=hash('sha256',$bucket.'|'.security_client_ip()); $file=$dir.'/'.$key.'.json';
    
$fp=@fopen($file,'c+'); if(!$fp) return true;
    if(!@
flock($fp,LOCK_EX)){@fclose($fp);return true;}
    
$raw=@stream_get_contents($fp); $d=json_decode($raw?:'{}',true)?:[];
    
$start=(int)($d['start']??$now); $count=(int)($d['count']??0);
    if(
$now-$start>=$window){$start=$now;$count=0;} $count++;
    
$payload=json_encode(['start'=>$start,'count'=>$count]);
    
$ok=@ftruncate($fp,0);
    if(
$ok){@rewind($fp);$ok=@fwrite($fp,$payload)!==false;@fflush($fp);}
    @
flock($fp,LOCK_UN);@fclose($fp);
    
// Если хранилище временно недоступно/переполнено, не ломаем игру и не создаём поток Notice.
    
if(!$ok) return true;
    return 
$count <= $limit;
}
function 
security_honeypot_ok(): bool { return empty($_POST['website']); }

function 
security_auth_sign(int $userId): string {
    
$cfg=require __DIR__.'/config.php';
    
$payload=$userId.'|'.time();
    
$sig=hash_hmac('sha256',$payload,(string)$cfg['app_key']);
    return 
rtrim(strtr(base64_encode($payload.'|'.$sig),'+/','-_'),'=');
}
function 
security_auth_user_id(): int {
    
$token=(string)($_COOKIE['auth'] ?? ''); if($token==='') return 0;
    
$raw=base64_decode(strtr($token,'-_','+/'),true); if($raw===false) return 0;
    
$parts=explode('|',$raw); if(count($parts)!==3) return 0;
    [
$id,$issued,$sig]=$parts; if(!ctype_digit($id)||!ctype_digit($issued)) return 0;
    if((int)
$issued time()-31536000) return 0;
    
$cfg=require __DIR__.'/config.php'$exp=hash_hmac('sha256',$id.'|'.$issued,(string)$cfg['app_key']);
    return 
hash_equals($exp,$sig)?(int)$id:0;
}
function 
security_auth_login(int $userId): void {
    if(
session_status()===PHP_SESSION_ACTIVEsession_regenerate_id(true);
    
$secure=!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off';
    
setcookie('id',(string)$userId,['expires'=>time()+31536000,'path'=>'/','secure'=>$secure,'httponly'=>true,'samesite'=>'Lax']);
    
setcookie('auth',security_auth_sign($userId),['expires'=>time()+31536000,'path'=>'/','secure'=>$secure,'httponly'=>true,'samesite'=>'Lax']);
    
setcookie('password','',['expires'=>time()-3600,'path'=>'/','secure'=>$secure,'httponly'=>true,'samesite'=>'Lax']);
}
function 
security_auth_logout(): void {
    foreach([
'id','auth','password'] as $nsetcookie($n,'',['expires'=>time()-3600,'path'=>'/','httponly'=>true,'samesite'=>'Lax']);
}

function 
security_password_hash(string $password): string { return password_hash($password,PASSWORD_DEFAULT); }
function 
security_password_verify(string $password,string $hash): bool {
    if(
str_starts_with($hash,'$2') || str_starts_with($hash,'$argon2')) return password_verify($password,$hash);
    return 
hash_equals($hash,md5(md5($password))); // migration-only legacy fallback
}


/** Одноразовый токен для цепочек регистрации/старта. */
function security_flow_token(string $name='start'bool $rotate=false): string {
    if (
session_status() !== PHP_SESSION_ACTIVEsession_start();
    
$current $_SESSION['_flow'][$name] ?? null;
    if (!
$rotate && is_array($current) && is_string($current['token'] ?? null) && (int)($current['expires'] ?? 0) >= time()) {
        return (string)
$current['token'];
    }
    
$token bin2hex(random_bytes(24));
    
$_SESSION['_flow'][$name] = ['token'=>$token,'expires'=>time()+(($name==='save')?3600:900)];
    return 
$token;
}
function 
security_flow_valid(string $token,string $name='start',bool $consume=false): bool {
    if (
session_status() !== PHP_SESSION_ACTIVEsession_start();
    
$row=$_SESSION['_flow'][$name] ?? null;
    if(!
is_array($row) || ($row['expires'] ?? 0) < time() || !is_string($row['token'] ?? null)) return false;
    
$ok=$token!=='' && hash_equals($row['token'],$token);
    if(
$ok && $consume) unset($_SESSION['_flow'][$name]);
    return 
$ok;
}
function 
security_app_key(): string {
    
$cfg=require __DIR__.'/config.php';
    return (string)(
$cfg['app_key'] ?? '');
}
function 
security_password_reset_token(array $user,int $expires): string {
    
$payload=(int)$user['id'].'|'.$expires;
    
$sig=hash_hmac('sha256',$payload.'|'.(string)$user['password'],security_app_key());
    return 
rtrim(strtr(base64_encode($payload.'|'.$sig),'+/','-_'),'=');
}
function 
security_password_reset_verify(string $token,array $user): bool {
    
$raw=base64_decode(strtr($token,'-_','+/'),true);
    if(
$raw===false) return false;
    
$parts=explode('|',$raw);
    if(
count($parts)!==|| !ctype_digit($parts[0]) || !ctype_digit($parts[1])) return false;
    [
$id,$expires,$sig]=$parts;
    if((int)
$id !== (int)$user['id'] || (int)$expires time()) return false;
    
$expected=hash_hmac('sha256',$id.'|'.$expires.'|'.(string)$user['password'],security_app_key());
    return 
hash_equals($expected,$sig);
}
Онлайн: 2
Реклама