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

+4 -3
View File
@@ -1,5 +1,5 @@
import { randomBytes } from "node:crypto"
import { extname, resolve } from "node:path"
import { resolve } from "node:path"
import {
rankProfileSchema,
@@ -46,6 +46,7 @@ import { db, schema } from "../db"
import { failure, parseBody, success } from "../http"
import { JudgeStatus } from "../judge/status"
import { getBooleanOption } from "../services/options"
import { sniffImageExtension } from "../services/image"
import { getUserProfileById } from "../services/profile"
import {
clientIp,
@@ -205,8 +206,8 @@ accountRoutes.post("/me/avatar", requireAuth, async (c) => {
return failure(c, 400, "invalid-file", "Invalid file content")
if (image.size > 2 * 1024 * 1024)
return failure(c, 400, "file-too-large", "Picture is too large")
const extension = extname(image.name).toLowerCase()
if (![".gif", ".jpg", ".jpeg", ".bmp", ".png"].includes(extension)) {
const extension = await sniffImageExtension(image)
if (!extension) {
return failure(c, 400, "unsupported-file", "Unsupported file format")
}
const filename = `${randomBytes(10).toString("hex")}${extension}`