/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); // 去除 外部引用与 标签,避免脚本跳转 $svg = preg_replace('#]*>|#i', '', $svg); return trim((string)$svg); } }