修复采购附件同源 XSS(07-17-purchase-attachment-xss-hardening)

这个提交包含在:
2026-07-17 20:32:05 +08:00
父节点 4c5eb7e22e
当前提交 f424492751
共修改 16 个文件,包含 2999 行新增和 211 行删除
+46
查看文件
@@ -333,6 +333,52 @@ export function mapApiErrorMessage(
return "请假冻结账本状态异常,请联系管理员处理。";
}
if (details?.attachment === "security_validation_failed") {
if (details?.reason === "quarantined") {
return "附件已隔离,无法下载,请联系管理员。";
}
if (details?.reason === "missing_object") {
return "附件文件不存在或已缺失,请联系管理员。";
}
return "附件未通过安全校验,请重新上传允许格式或联系管理员。";
}
if (details?.file === "empty_file") {
return "附件内容为空,请重新选择文件。";
}
if (details?.file === "size_limit" || details?.file === "file_too_large") {
return "附件超过大小限制,请压缩后重新上传。";
}
if (details?.file === "dangerous_filename") {
return "附件名称不安全,请重命名后重新上传。";
}
if (details?.file === "unsupported_type") {
return "仅支持 PDF、JPG、PNG、WEBP 文件,请重新选择。";
}
if (details?.file === "type_mismatch") {
return "附件扩展名、声明类型与实际内容不一致,请重新选择原始文件或转换为支持格式。";
}
if (details?.file === "corrupt_content") {
return "附件已损坏或内容不完整,请重新导出后上传。";
}
if (details?.file === "pixel_limit") {
return "图片尺寸过大,请压缩后重新上传。";
}
if (details?.file === "multiple_pages") {
return "暂不支持多页或动画图片,请转换为 PDF 或单页安全图片。";
}
if (details?.file === "only_single_file_supported") {
return "一次只能上传一个附件,请重新选择。";
}
if (details?.file === "required") {
return "当前假种要求上传附件后才能提交。";
}
+5 -4
查看文件
@@ -1,4 +1,4 @@
import { apiRequestData } from "./http";
import { apiRequestBlob, apiRequestData } from "./http";
import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance";
import type { PrecheckResponse, RiskFlag } from "./feeControl";
@@ -386,11 +386,12 @@ export async function deletePurchaseAttachment(purchaseRequestId: string, attach
);
}
export async function issuePurchaseAttachmentSignedUrl(purchaseRequestId: string, attachmentId: string, reason?: string) {
return apiRequestData<{ signedUrl: string; expiresAt: string }>(
`/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}/signed-url`,
export async function downloadPurchaseAttachment(purchaseRequestId: string, attachmentId: string, reason?: string) {
return apiRequestBlob(
`/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}/download`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(reason ? { reason } : {}),
},
);