fix(security): 附件 GET 端点防同源存储型 XSS——非白名单图片(svg/html等)强制下载+octet-stream+nosniff+CSP sandbox

maestro 双复审漏掉的真实 HIGH:上传文件与 API 同源,inline 渲染会在 console 源执行其脚本。只对 png/jpeg/gif/webp 允许 inline 预览,其余强制 attachment 下载并改 content-type 为 octet-stream,加 X-Content-Type-Options:nosniff + CSP sandbox 双重兜底。带 SVG 安全单测。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-30 01:59:34 +08:00
parent f443b16649
commit d9d3de26b8
2 changed files with 28 additions and 2 deletions
+9 -2
View File
@@ -375,12 +375,19 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
const dl = (req.query as { download?: string }).download;
const fallbackName = att.name || name;
// 安全:上传文件与本 API 同源,inline 渲染会在 console 源里执行其脚本 → 存储型 XSS。
// 故只对【硬白名单图片类型】允许 inline 预览;其余(尤其 image/svg+xml、text/html 等可携带脚本的类型)
// 一律强制 attachment 下载 + content-type 改 octet-stream,并加 nosniff + CSP sandbox 双重兜底。
const SAFE_INLINE = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
const inline = !dl && SAFE_INLINE.has(att.type ?? '');
reply
.header('cache-control', 'private, max-age=300')
.header('content-length', String(st.size))
.header('x-content-type-options', 'nosniff')
.header('content-security-policy', "sandbox; default-src 'none'")
.header('content-disposition',
`${dl ? 'attachment' : 'inline'}; filename*=UTF-8''${encodeURIComponent(fallbackName)}`)
.type(att.type || 'application/octet-stream');
`${inline ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(fallbackName)}`)
.type(inline ? (att.type as string) : 'application/octet-stream');
return reply.send(createReadStream(abs));
});