200 行
6.8 KiB
PHP
200 行
6.8 KiB
PHP
<?php
|
||
if (!defined('IN_APP')) {
|
||
http_response_code(403);
|
||
exit('Forbidden');
|
||
}
|
||
function validateUploadedImage($file, array $opts = [])
|
||
{
|
||
$opts = array_merge([
|
||
'allowed' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/x-icon', 'image/vnd.microsoft.icon'],
|
||
'maxBytes' => 2 * 1024 * 1024,
|
||
'minDim' => 0,
|
||
], $opts);
|
||
if (!isset($file['error']) || $file['error'] !== UPLOAD_ERR_OK) {
|
||
return ['ok' => false, 'error' => '上传错误,请重试'];
|
||
}
|
||
$tmp = $file['tmp_name'];
|
||
if (!is_uploaded_file($tmp)) {
|
||
return ['ok' => false, 'error' => '非法上传来源'];
|
||
}
|
||
$info = @getimagesize($tmp);
|
||
if (!$info || empty($info['mime'])) {
|
||
return ['ok' => false, 'error' => '不是有效的图片文件'];
|
||
}
|
||
$mime = $info['mime'];
|
||
$exifType = false;
|
||
if (function_exists('exif_imagetype')) {
|
||
$exifType = @exif_imagetype($tmp);
|
||
}
|
||
$extMap = [
|
||
IMAGETYPE_JPEG => 'jpg',
|
||
IMAGETYPE_PNG => 'png',
|
||
IMAGETYPE_GIF => 'gif',
|
||
IMAGETYPE_WEBP => 'webp',
|
||
IMAGETYPE_ICO => 'ico',
|
||
];
|
||
$ext = $extMap[$exifType ?: ($info[2] ?? 0)] ?? '';
|
||
if ($ext === '' || !in_array($mime, $opts['allowed'], true)) {
|
||
return ['ok' => false, 'error' => '不支持的图片格式(' . h($mime) . ')'];
|
||
}
|
||
if ($opts['minDim'] > 0 && ($info[0] < $opts['minDim'] || $info[1] < $opts['minDim'])) {
|
||
return ['ok' => false, 'error' => '图片尺寸不能小于 ' . $opts['minDim'] . '×' . $opts['minDim'] . ' 像素'];
|
||
}
|
||
if ((int) $file['size'] > (int) $opts['maxBytes']) {
|
||
return ['ok' => false, 'error' => '文件过大(上限 ' . round($opts['maxBytes'] / 1024 / 1024, 1) . 'MB)'];
|
||
}
|
||
$head = file_get_contents($tmp, false, null, 0, 8192);
|
||
if ($head !== false && preg_match('/<\\?php|<\\?=|\\?>\s*
|
||
return ['ok' => false, 'error' => '文件内容检测到不安全代码,已拒绝'];
|
||
}
|
||
return ['ok' => true, 'mime' => $mime, 'ext' => $ext, 'type' => $exifType ?: ($info[2] ?? 0), 'error' => ''];
|
||
}
|
||
function reencodeImage($tmp, $ext, $dest)
|
||
{
|
||
$dir = dirname($dest);
|
||
if (!is_dir($dir)) {
|
||
if (!@mkdir($dir, 0755, true) && !is_dir($dir)) {
|
||
return false;
|
||
}
|
||
}
|
||
if (!is_writable($dir)) {
|
||
return false;
|
||
}
|
||
if ($ext === 'ico') {
|
||
return @copy($tmp, $dest) && is_file($dest);
|
||
}
|
||
if (!extension_loaded('gd')) {
|
||
return @copy($tmp, $dest) && is_file($dest);
|
||
}
|
||
$src = null;
|
||
switch ($ext) {
|
||
case 'jpg': $src = @imagecreatefromjpeg($tmp); break;
|
||
case 'png': $src = @imagecreatefrompng($tmp); break;
|
||
case 'gif': $src = @imagecreatefromgif($tmp); break;
|
||
case 'webp': $src = @imagecreatefromwebp($tmp); break;
|
||
}
|
||
if (!$src) {
|
||
return @copy($tmp, $dest) && is_file($dest);
|
||
}
|
||
$ok = false;
|
||
if ($ext === 'png') $ok = @imagepng($src, $dest, 9);
|
||
elseif ($ext === 'gif') $ok = @imagegif($src, $dest);
|
||
elseif ($ext === 'webp') $ok = @imagewebp($src, $dest, 90);
|
||
else $ok = @imagejpeg($src, $dest, 90);
|
||
@imagedestroy($src);
|
||
return $ok && is_file($dest);
|
||
}
|
||
function cleanInput($str)
|
||
{
|
||
$str = (string) $str;
|
||
$str = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $str);
|
||
$str = preg_replace('
|
||
$str = preg_replace('
|
||
$str = preg_replace('
|
||
return trim($str);
|
||
}
|
||
function isBannedUsername($name)
|
||
{
|
||
$name = strtolower(trim((string) $name));
|
||
if ($name === '') {
|
||
return true;
|
||
}
|
||
$banned = [
|
||
'null', 'nil', 'undefined', 'nan', 'admin', 'root', 'administrator',
|
||
'superuser', 'system', 'guest', 'support', 'webmaster', 'postmaster',
|
||
'nobody', 'www', 'ftp', 'mail', 'root', 'sudo', 'owner', 'moderator',
|
||
];
|
||
if (in_array($name, $banned, true)) {
|
||
return true;
|
||
}
|
||
if (preg_match('/^\d+$/', $name) || mb_strlen($name, 'UTF-8') < 2) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function mailRateLimit($email)
|
||
{
|
||
$email = strtolower(trim((string) $email));
|
||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||
return ['allowed' => true, 'wait' => 0];
|
||
}
|
||
$file = __DIR__ . '/../storage/cache/mail_throttle.json';
|
||
$now = time();
|
||
$data = is_file($file) ? (json_decode(@file_get_contents($file), true) ?: []) : [];
|
||
foreach ($data as $k => $t) {
|
||
if ($now - (int) $t >= 300) {
|
||
unset($data[$k]);
|
||
}
|
||
}
|
||
if (isset($data[$email]) && $now - (int) $data[$email] < 300) {
|
||
return ['allowed' => false, 'wait' => 300 - ($now - (int) $data[$email])];
|
||
}
|
||
$data[$email] = $now;
|
||
@file_put_contents($file, json_encode($data));
|
||
return ['allowed' => true, 'wait' => 0];
|
||
}
|
||
function escLike($v)
|
||
{
|
||
return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], (string) $v);
|
||
}
|
||
function sqlInjectionGuard()
|
||
{
|
||
$patterns = [
|
||
'/\bUNION\b\s+\bSELECT\b/i',
|
||
'/\bOR\b\s+\d+\s*=\s*\d+/i',
|
||
'/\bAND\b\s+\d+\s*=\s*\d+/i',
|
||
'/--\s*$|
|
||
'/\bSLEEP\s*\(\s*\d+\s*\)/i',
|
||
'/\bBENCHMARK\s*\(/i',
|
||
'/\bINFORMATION_SCHEMA\b/i',
|
||
'/\bxp_cmdshell\b/i',
|
||
'/\bDROP\s+TABLE\b|\bTRUNCATE\b/i',
|
||
];
|
||
foreach (['_GET', '_COOKIE'] as $k) {
|
||
if (!isset($$k) || !is_array($$k)) {
|
||
continue;
|
||
}
|
||
foreach ($$k as $key => $val) {
|
||
if (!is_string($val)) {
|
||
continue;
|
||
}
|
||
foreach ($patterns as $p) {
|
||
if (preg_match($p, $val)) {
|
||
if (function_exists('Logger')) {
|
||
Logger::warn('疑似 SQL 注入请求被拦截', [
|
||
'ip' => getClientIp(),
|
||
'key' => $key,
|
||
'val' => mb_substr($val, 0, 200, 'UTF-8'),
|
||
]);
|
||
}
|
||
http_response_code(403);
|
||
exit('Access Denied');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function antiCrawlerGuard()
|
||
{
|
||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||
$l = strtolower($ua);
|
||
if (preg_match('/(googlebot|bingbot|baiduspider|yandexbot|duckduckbot|slurp|sogou|applebot|bytespider|twitterbot|facebookexternalhit)/i', $ua)) {
|
||
return;
|
||
}
|
||
$blockUa = [
|
||
'scrapy', 'python-requests', 'curl/', 'wget', 'libwww', 'nikto', 'sqlmap',
|
||
'masscan', 'nmap', 'zgrab', 'go-http-client', 'java/', 'httpclient',
|
||
'okhttp', 'phantomjs', 'headlesschrome', 'semrushbot', 'ahrefsbot', 'mj12bot',
|
||
];
|
||
foreach ($blockUa as $b) {
|
||
if (strpos($l, $b) !== false) {
|
||
http_response_code(403);
|
||
exit('Blocked by anti-crawler policy');
|
||
}
|
||
}
|
||
$script = $_SERVER['SCRIPT_NAME'] ?? '';
|
||
if (strpos($script, '/admin/') !== false || basename($script) === 'api.php') {
|
||
header('X-Robots-Tag: noindex, nofollow');
|
||
}
|
||
}
|