收紧输入渲染与后台配置权限
This commit is contained in:
@@ -174,18 +174,29 @@ class ChangelogController extends Controller
|
||||
*/
|
||||
private function saveChangelogNotification(DevChangelog $log): void
|
||||
{
|
||||
$typeLabel = DevChangelog::TYPE_CONFIG[$log->type]['label'] ?? '更新';
|
||||
$url = url('/changelog').'#v'.$log->version;
|
||||
// 广播文案允许保留安全链接,但标题与版本号必须先做 HTML 转义,避免系统消息被拼成恶意标签。
|
||||
$safeTypeLabel = e(DevChangelog::TYPE_CONFIG[$log->type]['label'] ?? '更新');
|
||||
$safeVersion = e((string) $log->version);
|
||||
$safeTitle = e((string) $log->title);
|
||||
$detailUrl = e($this->buildChangelogDetailUrl($log));
|
||||
|
||||
SaveMessageJob::dispatch([
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统公告',
|
||||
'to_user' => '大家',
|
||||
'content' => "📢 【版本更新 {$typeLabel}】v{$log->version}《{$log->title}》— <a href=\"{$url}\" target=\"_blank\" class=\"underline\">点击查看详情</a>",
|
||||
'content' => "📢 【版本更新 {$safeTypeLabel}】v{$safeVersion}《{$safeTitle}》— <a href=\"{$detailUrl}\" target=\"_blank\" rel=\"noopener\" class=\"underline\">点击查看详情</a>",
|
||||
'is_secret' => false,
|
||||
'font_color' => '#7c3aed',
|
||||
'action' => '',
|
||||
'sent_at' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成开发日志详情链接,并对版本片段做 URL 编码,避免广播 href 被注入额外属性。
|
||||
*/
|
||||
private function buildChangelogDetailUrl(DevChangelog $log): string
|
||||
{
|
||||
return route('changelog.index').'#v'.rawurlencode((string) $log->version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,28 +17,37 @@ use App\Models\SysParam;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* 类功能:后台通用系统参数配置控制器
|
||||
* 仅允许维护低敏公共参数,站长专属敏感配置需走各自独立页面。
|
||||
*/
|
||||
class SystemController extends Controller
|
||||
{
|
||||
/**
|
||||
* 构造函数注入聊天室状态服务
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 显示全局参数配置表单
|
||||
* 显示通用系统参数配置表单
|
||||
*/
|
||||
public function edit(): View
|
||||
{
|
||||
// 读取数据库中最新的参数 (剔除专属模块已接管的配置,避免重复显示)
|
||||
$params = SysParam::whereNotIn('alias', ['chatbot_enabled'])
|
||||
->where('alias', 'not like', 'smtp_%')
|
||||
->get()->pluck('body', 'alias')->toArray();
|
||||
$editableAliases = $this->editableSystemAliases();
|
||||
|
||||
// 为后台界面准备的文案对照 (可动态化或硬编码)
|
||||
$descriptions = SysParam::whereNotIn('alias', ['chatbot_enabled'])
|
||||
->where('alias', 'not like', 'smtp_%')
|
||||
->get()->pluck('guidetxt', 'alias')->toArray();
|
||||
// 通用系统页仅加载白名单字段,避免站长专属配置被普通高管查看。
|
||||
$systemParams = SysParam::query()
|
||||
->whereIn('alias', $editableAliases)
|
||||
->orderBy('id')
|
||||
->get(['alias', 'body', 'guidetxt']);
|
||||
|
||||
$params = $systemParams->pluck('body', 'alias')->all();
|
||||
$descriptions = $systemParams->pluck('guidetxt', 'alias')->all();
|
||||
|
||||
return view('admin.system.edit', compact('params', 'descriptions'));
|
||||
}
|
||||
@@ -48,16 +57,19 @@ class SystemController extends Controller
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->except(['_token', '_method']);
|
||||
// 只接受通用系统页白名单内的字段,忽略任何伪造提交的敏感键。
|
||||
$data = $request->only($this->editableSystemAliases());
|
||||
|
||||
foreach ($data as $alias => $body) {
|
||||
$normalizedBody = (string) $body;
|
||||
|
||||
SysParam::updateOrCreate(
|
||||
['alias' => $alias],
|
||||
['body' => $body]
|
||||
['body' => $normalizedBody]
|
||||
);
|
||||
|
||||
// 写入 Cache 保证极速读取
|
||||
$this->chatState->setSysParam($alias, $body);
|
||||
// 仅对白名单字段同步缓存,杜绝越权请求覆盖站长专属配置。
|
||||
$this->chatState->setSysParam($alias, $normalizedBody);
|
||||
|
||||
// 同时清除 Sysparam 模型的内部缓存
|
||||
SysParam::clearCache($alias);
|
||||
@@ -65,4 +77,31 @@ class SystemController extends Controller
|
||||
|
||||
return redirect()->route('admin.system.edit')->with('success', '系统参数已成功更新并生效!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用系统页允许维护的参数别名白名单
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function editableSystemAliases(): array
|
||||
{
|
||||
return SysParam::query()
|
||||
->orderBy('id')
|
||||
->pluck('alias')
|
||||
->filter(fn (string $alias): bool => ! $this->isSensitiveAlias($alias))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断参数是否属于站长专属敏感配置
|
||||
*/
|
||||
private function isSensitiveAlias(string $alias): bool
|
||||
{
|
||||
if (Str::startsWith($alias, ['smtp_', 'vip_payment_', 'wechat_bot_', 'chatbot_'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Str::endsWith($alias, ['_password', '_secret', '_token', '_key']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace App\Http\Requests;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* 聊天室发言请求验证器
|
||||
@@ -20,6 +21,27 @@ use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
*/
|
||||
class SendMessageRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* 允许前端提交的发言动作白名单。
|
||||
*/
|
||||
private const ALLOWED_ACTIONS = [
|
||||
'',
|
||||
'微笑',
|
||||
'大笑',
|
||||
'愤怒',
|
||||
'哭泣',
|
||||
'害羞',
|
||||
'鄙视',
|
||||
'得意',
|
||||
'疑惑',
|
||||
'同情',
|
||||
'无奈',
|
||||
'拳打',
|
||||
'飞吻',
|
||||
'偷看',
|
||||
'欢迎',
|
||||
];
|
||||
|
||||
/**
|
||||
* 判断当前请求是否允许继续。
|
||||
*/
|
||||
@@ -41,10 +63,22 @@ class SendMessageRequest extends FormRequest
|
||||
'to_user' => ['nullable', 'string', 'max:50'],
|
||||
'is_secret' => ['nullable', 'boolean'],
|
||||
'font_color' => ['nullable', 'string', 'max:10'], // html color hex
|
||||
'action' => ['nullable', 'string', 'max:50'], // 动作(例如:微笑着说)
|
||||
'action' => ['nullable', 'string', 'max:50', Rule::in(self::ALLOWED_ACTIONS)], // 动作字段仅允许预设值,阻断拼接式 XSS 注入
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在校验前统一整理输入,避免首尾空白绕过白名单判断。
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$action = $this->input('action');
|
||||
|
||||
$this->merge([
|
||||
'action' => is_string($action) ? trim($action) : $action,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回校验失败时的中文提示。
|
||||
*/
|
||||
@@ -57,6 +91,7 @@ class SendMessageRequest extends FormRequest
|
||||
'image.image' => '上传的文件必须是图片。',
|
||||
'image.mimes' => '仅支持 jpg、jpeg、png、gif、webp 图片格式。',
|
||||
'image.max' => '图片大小不能超过 6MB。',
|
||||
'action.in' => '发言动作不合法,请重新选择。',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,14 @@ namespace App\Http\Requests;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* 新建聊天室请求验证器
|
||||
* 负责限制建房权限并拦截危险的房间名称输入。
|
||||
*/
|
||||
class StoreRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
* 判断当前用户是否具备自建房间权限。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
@@ -26,24 +30,42 @@ class StoreRoomRequest extends FormRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
* 返回建房请求的校验规则。
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50', 'unique:rooms,room_name'],
|
||||
'name' => ['required', 'string', 'max:50', 'regex:/^[^<>]+$/u', 'unique:rooms,room_name'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在校验前整理房间输入,避免空白与危险字符绕过前端限制。
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$name = $this->input('name');
|
||||
$description = $this->input('description');
|
||||
|
||||
$this->merge([
|
||||
'name' => is_string($name) ? trim($name) : $name,
|
||||
'description' => is_string($description) ? trim($description) : $description,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回建房失败时的中文提示。
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '必须填写房间名称。',
|
||||
'name.unique' => '该房间名称已被占用。',
|
||||
'name.max' => '房间名称最多 50 个字符。',
|
||||
'name.regex' => '房间名称不能包含尖括号。',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* 修改聊天室设置请求验证器
|
||||
* 负责约束房间名称更新时的合法性,避免危险字符进入前端模板。
|
||||
*/
|
||||
class UpdateRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
* 判断当前请求是否允许继续。
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
@@ -24,23 +29,47 @@ class UpdateRoomRequest extends FormRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
* 返回修改房间设置的校验规则。
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50', 'unique:rooms,room_name,'.$this->route('id')],
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:50',
|
||||
'regex:/^[^<>]+$/u',
|
||||
Rule::unique('rooms', 'room_name')->ignore($this->route('id')),
|
||||
],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 在校验前整理更新表单,避免前后空白影响唯一性与安全判断。
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$name = $this->input('name');
|
||||
$description = $this->input('description');
|
||||
|
||||
$this->merge([
|
||||
'name' => is_string($name) ? trim($name) : $name,
|
||||
'description' => is_string($description) ? trim($description) : $description,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回房间设置更新失败时的中文提示。
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => '房间名称不能为空。',
|
||||
'name.unique' => '该房间名称已存在。',
|
||||
'name.regex' => '房间名称不能包含尖括号。',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user