/on* 事件 → 存储型 XSS,落盘前强制消毒;
* 4) 落盘文件名随机化,杜绝路径穿越与覆盖。
*/
protected function uploadFile(string $key): ?string
{
if (empty($_FILES[$key]['tmp_name'])) return null;
$f = $_FILES[$key];
if ($f['error'] !== UPLOAD_ERR_OK) return null;
if (!is_uploaded_file($f['tmp_name'])) return null;
if (($f['size'] ?? 0) > 8 * 1024 * 1024) return null; // 8MB 上限
$ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
if (!in_array($ext, $allowed, true)) return null;
// 真实类型校验(不信任扩展名与浏览器提交的 MIME)
$realMime = function_exists('finfo_open')
? (finfo_file(($fi = finfo_open(FILEINFO_MIME_TYPE)), $f['tmp_name']) ?: '') : '';
if (isset($fi) && $fi) { finfo_close($fi); }
$isSvg = ($ext === 'svg');
if (!$isSvg) {
// 光栅图:必须能被 GD 识别为真实图像,且 MIME 属于图片类
$info = @getimagesize($f['tmp_name']);
$okMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if ($info === false) return null;
if ($realMime && !in_array($realMime, $okMimes, true)) return null;
} else {
// SVG:类型须为 svg/xml/text,随后消毒内容
$svgMimes = ['image/svg+xml', 'text/plain', 'text/xml', 'application/xml'];
if ($realMime && !in_array($realMime, $svgMimes, true)) return null;
}
$dir = BASE_PATH . '/public/assets/uploads';
if (!is_dir($dir)) mkdir($dir, 0755, true);
$name = uniqid('u_') . '.' . $ext;
$dest = $dir . '/' . $name;
if ($isSvg) {
// 读取 → 消毒 → 写入(不使用 move_uploaded_file,因内容已被改写)
$raw = @file_get_contents($f['tmp_name']);
if ($raw === false) return null;
$clean = $this->sanitizeSvg($raw);
if ($clean === '' || @file_put_contents($dest, $clean) === false) return null;
} else {
if (!move_uploaded_file($f['tmp_name'], $dest)) return null;
}
return 'assets/uploads/' . $name;
}
/** 消毒 SVG:移除脚本、事件处理器与危险协议,阻断存储型 XSS */
protected function sanitizeSvg(string $svg): string
{
// 去除
$svg = preg_replace('##is', '', $svg);
// 去除 (可嵌 HTML/脚本)
$svg = preg_replace('#]*>.*?#is', '', $svg);
// 去除内联事件处理器 on*="..."
$svg = preg_replace('#\son\w+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)#i', '', $svg);
// 去除 javascript: / data:text 等危险协议引用
$svg = preg_replace('#(href|xlink:href)\s*=\s*("|\')?\s*javascript:[^"\'>]*#i', '', $svg);
// 去除