fix(上传): 头像和题面插图按文件头认格式,不信扩展名

原来只看扩展名:改个后缀什么都能落盘,再由 /public/* 按扩展名配 content-type
发出去。线上 Caddy 有 nosniff 兜着,但不该把非图片存到盘上。

- services/image.ts:按 PNG / JPEG / GIF / BMP 的文件头识别,返回落盘用的扩展名 ——
  以内容为准,改名成 .png 的 JPEG 存成 .jpg,content-type 才对得上
- 头像上传、后台插图上传都走它;插图那边扩展名白名单保留当第一道快速筛
- serveUpload 自己也带 x-content-type-options: nosniff,不只靠 Caddy

实跑核过:真 PNG / GIF 正常;改名成 .png 的 JPEG 存成 .jpg;改名成 .png 的 HTML
两个入口都拒;/public/upload 回 image/png + nosniff。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 15:15:36 +08:00
1 parent b105fe3cbd
commit e17da63fc6
4 files changed
+38 -6

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
/**
* 按文件头认图片格式,不信文件名。头像和题面插图原来只看扩展名:改个后缀什么都能
* 落盘,再由 /public/* 按扩展名配 content-type 发出去。
*
* 返回值就是落盘该用的扩展名 —— 以内容为准,一张改名成 .png 的 JPEG 会存成 .jpg,
* 浏览器拿到的 content-type 才对得上。
*/
const SIGNATURES: [extension: string, bytes: number[]][] = [
[".png", [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],
[".jpg", [0xff, 0xd8, 0xff]],
[".gif", [0x47, 0x49, 0x46, 0x38]], // GIF8(GIF87a / GIF89a)
[".bmp", [0x42, 0x4d]], // BM
]
export async function sniffImageExtension(file: Blob) {
const head = new Uint8Array(await file.slice(0, 8).arrayBuffer())
for (const [extension, bytes] of SIGNATURES) {
if (bytes.every((byte, index) => head[index] === byte)) return extension
}
return null
}