修复认证与基础安全链路

This commit is contained in:
2026-04-19 14:42:42 +08:00
parent bd97ed0b73
commit 5ce83a769d
13 changed files with 636 additions and 55 deletions
@@ -9,7 +9,7 @@
* 安全保证:
* - 路由被 ['chat.auth', 'chat.has_position', 'chat.level:super'] 三层中间件保护
* - 普通用户无权访问此接口,无法伪造对他人的广播
* - options 中的用户输入字段在后端经过 strip_tags 清洗
* - options 中的用户输入字段在后端统一降级为纯文本 / 白名单样式值
*
* @author ChatRoom Laravel
*
@@ -23,6 +23,9 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 类功能:安全地下发大卡片广播消息。
*/
class BannerBroadcastController extends Controller
{
/**
@@ -46,23 +49,39 @@ class BannerBroadcastController extends Controller
'options.body' => ['nullable', 'string', 'max:500'],
'options.sub' => ['nullable', 'string', 'max:200'],
'options.gradient' => ['nullable', 'array', 'max:5'],
'options.gradient.*' => ['nullable', 'string', 'max:30'],
'options.titleColor' => ['nullable', 'string', 'max:30'],
'options.autoClose' => ['nullable', 'integer', 'min:0', 'max:30000'],
'options.buttons' => ['nullable', 'array', 'max:4'],
'options.buttons.*.label' => ['nullable', 'string', 'max:30'],
'options.buttons.*.color' => ['nullable', 'string', 'max:30'],
'options.buttons.*.action' => ['nullable', 'string', 'max:20'],
]);
// 对可能包含用户输入的字段进行 HTML 净化(防 XSS)
// 所有可见文案一律降级为纯文本,避免允许标签残留属性后在前端 innerHTML 中执行。
$opts = $validated['options'];
foreach (['title', 'name', 'body', 'sub'] as $field) {
if (isset($opts[$field])) {
$opts[$field] = strip_tags($opts[$field], '<b><strong><em><span><br>');
$opts[$field] = $this->sanitizeBannerText($opts[$field]);
}
}
// 按钮 label 不允许 HTML
if (isset($opts['titleColor'])) {
$opts['titleColor'] = $this->sanitizeCssValue($opts['titleColor'], '#fde68a');
}
if (! empty($opts['gradient'])) {
$opts['gradient'] = array_values(array_map(
fn ($color) => $this->sanitizeCssValue($color, '#4f46e5'),
$opts['gradient']
));
}
// 按钮 label 与颜色都只允许安全文本 / 颜色值。
if (! empty($opts['buttons'])) {
$opts['buttons'] = array_map(function ($btn) {
$btn['label'] = strip_tags($btn['label'] ?? '');
$btn['color'] = preg_replace('/[^a-z0-9#(),\s.%rgba\/]/i', '', $btn['color'] ?? '#10b981');
$btn['label'] = $this->sanitizeBannerText($btn['label'] ?? '');
$btn['color'] = $this->sanitizeCssValue($btn['color'] ?? '#10b981', '#10b981');
// action 只允许预定义值,防止注入任意 JS
$btn['action'] = in_array($btn['action'] ?? '', ['close', 'add_friend', 'remove_friend', 'link'])
? $btn['action'] : 'close';
@@ -79,4 +98,38 @@ class BannerBroadcastController extends Controller
return response()->json(['status' => 'success', 'message' => '广播已发送']);
}
/**
* Banner 文案净化为安全纯文本。
*/
private function sanitizeBannerText(?string $text): string
{
return trim(strip_tags((string) $text));
}
/**
* 清洗颜色 / 渐变等 CSS 值,阻断样式属性注入。
*/
private function sanitizeCssValue(?string $value, string $default): string
{
$sanitized = strtolower(trim((string) $value));
if ($sanitized === '' || preg_match('/(?:javascript|expression|url\s*\(|data:|var\s*\()/i', $sanitized)) {
return $default;
}
$allowedPatterns = [
'/^#[0-9a-f]{3,8}$/i',
'/^rgba?\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/i',
'/^hsla?\(\s*\d{1,3}(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)$/i',
'/^(?:white|black|red|blue|green|gray|grey|yellow|orange|pink|purple|teal|cyan|indigo|amber|emerald|transparent|currentcolor)$/i',
];
foreach ($allowedPatterns as $allowedPattern) {
if (preg_match($allowedPattern, $sanitized)) {
return $sanitized;
}
}
return $default;
}
}