远程下载与海报合成增加SSRF防护
This commit is contained in:
@@ -353,6 +353,9 @@ class UserLogic extends LogicBase
|
||||
$avatar = download_file($avatar_url, 'uploads/user/avatar/', $file_name);
|
||||
} else {
|
||||
$avatar = 'uploads/user/avatar/' . md5($user_id . $time . rand(10000, 99999)) . '.jpeg';
|
||||
if (!check_url_safety($avatar_url)) {
|
||||
throw new Exception('头像地址不合法');
|
||||
}
|
||||
$StorageDriver = new StorageDriver($config);
|
||||
if (!$StorageDriver->fetch($avatar_url, $avatar)) {
|
||||
throw new Exception('头像保存失败:' . $StorageDriver->getError());
|
||||
@@ -541,4 +544,4 @@ class UserLogic extends LogicBase
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ class UserServer
|
||||
$avatar = download_file($avatar_url, 'uploads/user/avatar/', $file_name);
|
||||
} else {
|
||||
$avatar = 'uploads/user/avatar/' . md5($openid . $time) . '.jpeg';
|
||||
if (!check_url_safety($avatar_url)) {
|
||||
throw new Exception('头像地址不合法');
|
||||
}
|
||||
$StorageDriver = new StorageDriver($config);
|
||||
if (!$StorageDriver->fetch($avatar_url, $avatar)) {
|
||||
throw new Exception( '头像保存失败:'. $StorageDriver->getError());
|
||||
@@ -200,6 +203,9 @@ class UserServer
|
||||
$avatar = download_file($avatar_url, 'uploads/user/avatar/', $file_name);
|
||||
} else {
|
||||
$avatar = 'uploads/user/avatar/' . md5($openid . $time) . '.jpeg';
|
||||
if (!check_url_safety($avatar_url)) {
|
||||
throw new Exception('头像地址不合法');
|
||||
}
|
||||
$StorageDriver = new StorageDriver($config);
|
||||
if (!$StorageDriver->fetch($avatar_url, $avatar)) {
|
||||
throw new Exception( '头像保存失败:'. $StorageDriver->getError());
|
||||
@@ -243,4 +249,4 @@ class UserServer
|
||||
|
||||
return $user_info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,238 @@ function data_error($msg = '', $data = [], $code = 0, $show = 1)
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 判断IP是否为内网/回环/链路本地等不安全地址
|
||||
* @param string $ip
|
||||
* @return bool
|
||||
*/
|
||||
function is_private_ip($ip)
|
||||
{
|
||||
if (!is_string($ip) || !filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$long = sprintf('%u', ip2long($ip));
|
||||
$inRange = function ($min, $max) use ($long) {
|
||||
return $long >= $min && $long <= $max;
|
||||
};
|
||||
if ($inRange(0, 16777215)) return true; // 0.0.0.0/8
|
||||
if ($inRange(167772160, 184549375)) return true; // 10.0.0.0/8
|
||||
if ($inRange(2130706432, 2147483647)) return true; // 127.0.0.0/8 回环
|
||||
if ($inRange(2851995648, 2852061183)) return true; // 169.254.0.0/16 链路本地
|
||||
if ($inRange(2886729728, 2887778303)) return true; // 172.16.0.0/12
|
||||
if ($inRange(3232235520, 3232301055)) return true; // 192.168.0.0/16
|
||||
if ($inRange(1681915904, 1686110207)) return true; // 100.64.0.0/10 CGNAT
|
||||
if ($inRange(3221225472, 3221225727)) return true; // 192.0.0.0/24
|
||||
if ($inRange(3221225984, 3221226239)) return true; // 192.0.2.0/24
|
||||
if ($inRange(3323068416, 3323199487)) return true; // 198.18.0.0/15
|
||||
if ($inRange(3325256704, 3325256959)) return true; // 198.51.100.0/24
|
||||
if ($inRange(3405803776, 3405804031)) return true; // 203.0.113.0/24
|
||||
if ($inRange(3758096384, 4026531839)) return true; // 224.0.0.0/4 组播
|
||||
if ($inRange(4026531840, 4294967295)) return true; // 240.0.0.0/4 保留
|
||||
return false;
|
||||
}
|
||||
|
||||
// IPv6
|
||||
$packed = inet_pton($ip);
|
||||
if (false === $packed) {
|
||||
return true;
|
||||
}
|
||||
$hex = bin2hex($packed);
|
||||
if ($hex === str_repeat('0', 31) . '1') return true; // ::1 回环
|
||||
if ($hex === str_repeat('0', 32)) return true; // :: 未指定
|
||||
if (strncmp($hex, 'fc', 2) === 0 || strncmp($hex, 'fd', 2) === 0) return true; // fc00::/7
|
||||
if (strncmp($hex, 'fe8', 3) === 0 || strncmp($hex, 'fe9', 3) === 0
|
||||
|| strncmp($hex, 'fea', 3) === 0 || strncmp($hex, 'feb', 3) === 0) return true; // fe80::/10
|
||||
if (strncmp($hex, 'ff', 2) === 0) return true; // ff00::/8 组播
|
||||
// ::ffff:0:0/96 IPv4 映射地址
|
||||
if (substr($hex, 0, 20) === str_repeat('0', 20) && substr($hex, 20, 4) === 'ffff') {
|
||||
$v4 = long2ip(hexdec(substr($hex, 24, 8)));
|
||||
return is_private_ip($v4);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 校验网络资源地址是否安全(仅允许 http/https 且解析后非内网地址)
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
function check_url_safety($url)
|
||||
{
|
||||
if (empty($url) || !is_string($url)) {
|
||||
return false;
|
||||
}
|
||||
$scheme = strtolower((string)parse_url($url, PHP_URL_SCHEME));
|
||||
if (!in_array($scheme, ['http', 'https'], true)) {
|
||||
return false;
|
||||
}
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if (empty($host) || !is_string($host)) {
|
||||
return false;
|
||||
}
|
||||
$host = rtrim(trim($host, '[]'), '.');
|
||||
if (empty($host)) {
|
||||
return false;
|
||||
}
|
||||
return !empty(safe_resolve_ips($host));
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 解析域名并返回全部安全的IP(任一解析结果为不安全地址则整体拒绝)
|
||||
* @param string $host
|
||||
* @return array
|
||||
*/
|
||||
function safe_resolve_ips($host)
|
||||
{
|
||||
if (!is_string($host) || empty($host)) {
|
||||
return [];
|
||||
}
|
||||
// IP 字面量直接判断
|
||||
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
return is_private_ip($host) ? [] : [$host];
|
||||
}
|
||||
// 域名解析后逐个判断
|
||||
$ips = gethostbynamel($host);
|
||||
if (false === $ips || empty($ips)) {
|
||||
return [];
|
||||
}
|
||||
foreach ($ips as $ip) {
|
||||
if (is_private_ip($ip)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return $ips;
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 安全HTTP请求(校验URL、固定已解析IP、限时限量、不跟随重定向)
|
||||
* @param string $url
|
||||
* @param callable|null $write_callback 响应体逐块回调,返回写入字节数
|
||||
* @param bool $head 是否只发起 HEAD 请求
|
||||
* @param int $max_size 最大下载字节数
|
||||
* @param int $timeout 总超时秒数
|
||||
* @return bool
|
||||
*/
|
||||
function safe_http_fetch($url, $write_callback = null, $head = false, $max_size = 157286400, $timeout = 30)
|
||||
{
|
||||
if (!check_url_safety($url)) {
|
||||
return false;
|
||||
}
|
||||
$scheme = strtolower((string)parse_url($url, PHP_URL_SCHEME));
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if (empty($host) || !is_string($host)) {
|
||||
return false;
|
||||
}
|
||||
$host = rtrim(trim($host, '[]'), '.');
|
||||
$ips = safe_resolve_ips($host);
|
||||
if (empty($ips)) {
|
||||
return false;
|
||||
}
|
||||
$port = (int)parse_url($url, PHP_URL_PORT);
|
||||
if ($port <= 0) {
|
||||
$port = ($scheme === 'https') ? 443 : 80;
|
||||
}
|
||||
$ch = curl_init();
|
||||
if (false === $ch) {
|
||||
return false;
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, max(1, (int)$timeout));
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
|
||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 0);
|
||||
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
|
||||
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
|
||||
// 固定已校验的解析结果,缓解 DNS rebinding
|
||||
$resolve = [];
|
||||
foreach ($ips as $ip) {
|
||||
$resolve[] = $host . ':' . $port . ':' . $ip;
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_RESOLVE, $resolve);
|
||||
|
||||
$downloaded = 0;
|
||||
$aborted = false;
|
||||
if ($head) {
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
|
||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use ($write_callback, &$downloaded, $max_size, &$aborted) {
|
||||
$len = strlen($data);
|
||||
if ($downloaded + $len > $max_size) {
|
||||
$aborted = true;
|
||||
return 0;
|
||||
}
|
||||
$downloaded += $len;
|
||||
if (is_callable($write_callback)) {
|
||||
return (int)$write_callback($data);
|
||||
}
|
||||
return $len;
|
||||
});
|
||||
}
|
||||
|
||||
$result = curl_exec($ch);
|
||||
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($ch);
|
||||
if (false === $result || $aborted || !in_array($status, [200, 304], true)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 安全获取远程文件内容
|
||||
* @param string $url
|
||||
* @param int $max_size 最大下载字节数
|
||||
* @param int $timeout 总超时秒数
|
||||
* @return bool|string
|
||||
*/
|
||||
function safe_get_remote_content($url, $max_size = 157286400, $timeout = 30)
|
||||
{
|
||||
$content = '';
|
||||
$ok = safe_http_fetch($url, function ($data) use (&$content) {
|
||||
$content .= $data;
|
||||
return strlen($data);
|
||||
}, false, $max_size, $timeout);
|
||||
if (!$ok || $content === '') {
|
||||
return false;
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 安全读取本地文件或远程文件内容(远程仅允许 http/https)
|
||||
* @param string $uri
|
||||
* @return bool|string
|
||||
*/
|
||||
function safe_read_file($uri)
|
||||
{
|
||||
if (!is_string($uri) || empty($uri)) {
|
||||
return false;
|
||||
}
|
||||
if (strpos($uri, '://') !== false) {
|
||||
$scheme = strtolower((string)parse_url($uri, PHP_URL_SCHEME));
|
||||
if (!in_array($scheme, ['http', 'https'], true)) {
|
||||
return false;
|
||||
}
|
||||
return safe_get_remote_content($uri);
|
||||
}
|
||||
if (!is_file($uri) || !is_readable($uri)) {
|
||||
return false;
|
||||
}
|
||||
$content = file_get_contents($uri);
|
||||
return false === $content ? false : $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* User: 意象信息科技 lr
|
||||
* Desc: 下载文件
|
||||
@@ -107,22 +339,24 @@ function data_error($msg = '', $data = [], $code = 0, $show = 1)
|
||||
*/
|
||||
function download_file($url, $save_dir, $file_name)
|
||||
{
|
||||
if (!check_url_safety($url)) {
|
||||
return '';
|
||||
}
|
||||
if (!file_exists($save_dir)) {
|
||||
mkdir($save_dir, 0775, true);
|
||||
}
|
||||
$file_src = $save_dir . $file_name;
|
||||
file_exists($file_src) && unlink($file_src);
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
|
||||
$file = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
$resource = fopen($file_src, 'a');
|
||||
fwrite($resource, $file);
|
||||
fclose($resource);
|
||||
if (filesize($file_src) == 0) {
|
||||
unlink($file_src);
|
||||
$fp = fopen($file_src, 'wb');
|
||||
if (false === $fp) {
|
||||
return '';
|
||||
}
|
||||
$ok = safe_http_fetch($url, function ($data) use ($fp) {
|
||||
return fwrite($fp, $data);
|
||||
}, false, 157286400, 30);
|
||||
fclose($fp);
|
||||
if (!$ok || filesize($file_src) == 0) {
|
||||
file_exists($file_src) && unlink($file_src);
|
||||
return '';
|
||||
}
|
||||
return $file_src;
|
||||
@@ -553,11 +787,7 @@ function check_file_exists($file)
|
||||
{
|
||||
//远程文件
|
||||
if ('http' == strtolower(substr($file, 0, 4))) {
|
||||
|
||||
$header = get_headers($file, true);
|
||||
|
||||
return isset($header[0]) && (strpos($header[0], '200') || strpos($header[0], '304'));
|
||||
|
||||
return safe_http_fetch($file, null, true, 157286400, 30);
|
||||
} else {
|
||||
|
||||
return file_exists($file);
|
||||
@@ -1072,5 +1302,3 @@ function check_is_video($video) : bool
|
||||
|
||||
return strpos($type, 'video') !== false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -131,7 +131,18 @@ class QrCodeLogic extends LogicBase{
|
||||
|
||||
$qr_code_logic = new QrCodeLogic();
|
||||
//获取背景图
|
||||
$share_background_img = imagecreatefromstring(file_get_contents($background_img));
|
||||
$background_content = safe_read_file($background_img);
|
||||
if (false === $background_content) {
|
||||
throw new Exception('背景图读取失败');
|
||||
}
|
||||
$background_size = getimagesizefromstring($background_content);
|
||||
if (false === $background_size) {
|
||||
throw new Exception('背景图读取失败');
|
||||
}
|
||||
$share_background_img = imagecreatefromstring($background_content);
|
||||
if (false === $share_background_img) {
|
||||
throw new Exception('背景图解析失败');
|
||||
}
|
||||
|
||||
//合成头像
|
||||
$qr_code_logic->writeImg($share_background_img, $user_avatar, $poster_config['head_pic'],true);
|
||||
@@ -153,7 +164,7 @@ class QrCodeLogic extends LogicBase{
|
||||
$qr_code_logic->writeText($share_background_img, floatval($goods['min_price']), $poster_config['price']);
|
||||
|
||||
//合成商品标题
|
||||
$goods_name = auto_adapt($poster_config['title']['font_size'], 0, $poster_config['title']['font_face'], $goods['name'], $poster_config['title']['w'],$poster_config['title']['y'],getimagesize($background_img));
|
||||
$goods_name = auto_adapt($poster_config['title']['font_size'], 0, $poster_config['title']['font_face'], $goods['name'], $poster_config['title']['w'],$poster_config['title']['y'],$background_size);
|
||||
$qr_code_logic->writeText($share_background_img, $goods_name, $poster_config['title']);
|
||||
|
||||
//合成二维码
|
||||
@@ -323,7 +334,18 @@ class QrCodeLogic extends LogicBase{
|
||||
$qr_code_logic = new QrCodeLogic();
|
||||
|
||||
//获取背景图
|
||||
$share_background_img = imagecreatefromstring(file_get_contents($background_img));
|
||||
$background_content = safe_read_file($background_img);
|
||||
if (false === $background_content) {
|
||||
throw new Exception('背景图读取失败');
|
||||
}
|
||||
$background_size = getimagesizefromstring($background_content);
|
||||
if (false === $background_size) {
|
||||
throw new Exception('背景图读取失败');
|
||||
}
|
||||
$share_background_img = imagecreatefromstring($background_content);
|
||||
if (false === $share_background_img) {
|
||||
throw new Exception('背景图解析失败');
|
||||
}
|
||||
//合成头像
|
||||
$qr_code_logic->writeImg($share_background_img, $user_avatar, $poster_config['head_pic'],true);
|
||||
|
||||
@@ -334,7 +356,7 @@ class QrCodeLogic extends LogicBase{
|
||||
$notice = '长按识别二维码 >>';
|
||||
$qr_code_logic->writeText($share_background_img, $notice, $poster_config['notice']);
|
||||
//合成商品标题
|
||||
$title = auto_adapt($poster_config['title']['font_size'], 0, $poster_config['title']['font_face'], '邀请你一起来赚大钱', $poster_config['title']['w'],$poster_config['title']['y'],getimagesize($background_img));
|
||||
$title = auto_adapt($poster_config['title']['font_size'], 0, $poster_config['title']['font_face'], '邀请你一起来赚大钱', $poster_config['title']['w'],$poster_config['title']['y'],$background_size);
|
||||
$qr_code_logic->writeText($share_background_img, $title, $poster_config['title']);
|
||||
//邀请码
|
||||
$qr_code_logic->writeText($share_background_img, '邀请码 '.$user['distribution_code'], $poster_config['code_text']);
|
||||
@@ -468,7 +490,18 @@ class QrCodeLogic extends LogicBase{
|
||||
|
||||
$qr_code_logic = new QrCodeLogic();
|
||||
//获取背景图
|
||||
$share_background_img = imagecreatefromstring(file_get_contents($background_img));
|
||||
$background_content = safe_read_file($background_img);
|
||||
if (false === $background_content) {
|
||||
throw new Exception('背景图读取失败');
|
||||
}
|
||||
$background_size = getimagesizefromstring($background_content);
|
||||
if (false === $background_size) {
|
||||
throw new Exception('背景图读取失败');
|
||||
}
|
||||
$share_background_img = imagecreatefromstring($background_content);
|
||||
if (false === $share_background_img) {
|
||||
throw new Exception('背景图解析失败');
|
||||
}
|
||||
|
||||
//合成头像
|
||||
$qr_code_logic->writeImg($share_background_img, $user_avatar, $poster_config['head_pic'],true);
|
||||
@@ -494,7 +527,7 @@ class QrCodeLogic extends LogicBase{
|
||||
$qr_code_logic->writeText($share_background_img, $brief_title, $poster_config['brief_title']);
|
||||
|
||||
//合成商品标题
|
||||
$goods_name = auto_adapt($poster_config['title']['font_size'], 0, $poster_config['title']['font_face'], $bargain_launch['goods_snap']['name'], $poster_config['title']['w'],$poster_config['title']['y'],getimagesize($background_img));
|
||||
$goods_name = auto_adapt($poster_config['title']['font_size'], 0, $poster_config['title']['font_face'], $bargain_launch['goods_snap']['name'], $poster_config['title']['w'],$poster_config['title']['y'],$background_size);
|
||||
$qr_code_logic->writeText($share_background_img, $goods_name, $poster_config['title']);
|
||||
|
||||
//合成二维码
|
||||
@@ -555,7 +588,14 @@ class QrCodeLogic extends LogicBase{
|
||||
|
||||
//写入图片
|
||||
public function writeImg($poster, $img_uri, $config, $is_rounded = false){
|
||||
$pic_img = imagecreatefromstring(file_get_contents($img_uri));
|
||||
$img_content = safe_read_file($img_uri);
|
||||
if (false === $img_content) {
|
||||
throw new Exception('图片读取失败');
|
||||
}
|
||||
$pic_img = imagecreatefromstring($img_content);
|
||||
if (false === $pic_img) {
|
||||
throw new Exception('图片解析失败');
|
||||
}
|
||||
$is_rounded?$pic_img = rounded_corner($pic_img):'';//切成圆角返回头像资源
|
||||
$pic_w = imagesx($pic_img);
|
||||
$pic_h = imagesy($pic_img);
|
||||
@@ -591,4 +631,4 @@ class QrCodeLogic extends LogicBase{
|
||||
return $poster;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user