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

这个提交包含在:
2026-07-17 20:32:05 +08:00
父节点 4c5eb7e22e
当前提交 f424492751
共修改 16 个文件,包含 2999 行新增和 211 行删除
@@ -134,6 +134,10 @@ const defaultCloseoutReimbursementId = "40876ef3-0652-4972-aa14-1aad1a855ae0";
const defaultCloseoutReimbursementCode = "BX-20260514-0016";
const wecomUa =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/4.1.20 MicroMessenger/8.0.1 Language/zh_CN";
const safePurchaseAttachmentPng = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADUlEQVR4nGP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg==",
"base64",
);
function nowStamp() {
return `${trialrunMarkerPrefix}-${Date.now()}`;
@@ -1029,15 +1033,15 @@ test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请
await page.locator('[data-testid^="purchase-item-quantity-"]:visible').first().fill("1");
await page.locator('[data-testid^="purchase-item-unit-price-"]:visible').first().fill("188");
const purchaseAttachmentName = `${trialrunSafePrefix.toLowerCase()}-purchase-attachment-${marker}.txt`;
const purchaseAttachmentName = `${trialrunSafePrefix.toLowerCase()}-purchase-attachment-${marker}.png`;
const uploadAttachmentResponsePromise = page.waitForResponse((response) => {
const url = new URL(response.url());
return response.request().method() === "POST" && url.pathname === `/api/purchase-requests/${purchaseRequestId}/attachments`;
});
await page.getByTestId("purchase-attachment-file-input").setInputFiles({
name: purchaseAttachmentName,
mimeType: "text/plain",
buffer: Buffer.from(`${trialrunMarkerPrefix} purchase attachment ${marker}\n`, "utf-8"),
mimeType: "image/png",
buffer: safePurchaseAttachmentPng,
});
const uploadAttachmentResponse = await uploadAttachmentResponsePromise;
if (!uploadAttachmentResponse.ok()) {
@@ -45,6 +45,12 @@ const me = {
const purchaseId = "purchase-r103";
const reimbursementId = "11111111-1111-4111-8111-111111111111";
const invoiceId = "invoice-r103";
const safePurchaseAttachmentId = "attachment-safe-r103";
const quarantinedPurchaseAttachmentId = "attachment-quarantined-r103";
const safePngBuffer = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADUlEQVR4nGP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg==",
"base64",
);
function navigationItem(key: string, href: string, label: string, icon: string) {
return { key, href, label, icon, placement: "main", permissionCodes: [], featureFlagKey: null };
@@ -97,7 +103,48 @@ function purchaseDetail() {
updatedAt: "2026-05-06T09:10:00.000Z",
},
],
attachments: [],
attachments: [
{
id: safePurchaseAttachmentId,
originalName: "采购报价.png",
mimeType: "image/png",
fileSize: safePngBuffer.length,
fileSha256: null,
governanceStatus: "approved",
sensitivityLevel: "internal",
attachmentType: "quote",
downloadPolicy: "reason_required",
reviewedBy: null,
reviewedAt: null,
reviewNote: null,
archivedBy: null,
archivedAt: null,
archiveNote: null,
uploadedBy: me.id,
createdAt: "2026-05-06T09:20:00.000Z",
uploader: { id: me.id, employeeNo: me.employeeNo, name: me.name },
},
{
id: quarantinedPurchaseAttachmentId,
originalName: "历史隔离附件.pdf",
mimeType: "application/pdf",
fileSize: 128,
fileSha256: null,
governanceStatus: "quarantined",
sensitivityLevel: "internal",
attachmentType: "quote",
downloadPolicy: "standard",
reviewedBy: null,
reviewedAt: null,
reviewNote: null,
archivedBy: null,
archivedAt: null,
archiveNote: null,
uploadedBy: me.id,
createdAt: "2026-05-06T09:15:00.000Z",
uploader: { id: me.id, employeeNo: me.employeeNo, name: me.name },
},
],
pendingWorkflow: null,
archiveOrder: null,
permissions: {
@@ -300,12 +347,38 @@ async function mockApis(page: Page) {
}
if (pathName === `/api/purchase-requests/${purchaseId}/attachments` && request.method() === "POST") {
if (request.postDataBuffer()?.includes(Buffer.from('filename="unsupported.txt"'))) {
await route.fulfill({
status: 400,
contentType: "application/json",
body: JSON.stringify({
requestId: "mock-r103-unsupported-attachment",
error: {
code: "VALID_001",
message: "参数校验失败",
details: { file: "unsupported_type" },
},
}),
});
return;
}
await fulfillJson(route, {
attachment: {
id: "attachment-r103",
originalName: "采购报价.pdf",
mimeType: "application/pdf",
fileSize: 4,
originalName: "采购报价.png",
mimeType: "image/png",
fileSize: safePngBuffer.length,
fileSha256: null,
governanceStatus: "approved",
sensitivityLevel: "internal",
attachmentType: "quote",
downloadPolicy: "standard",
reviewedBy: null,
reviewedAt: null,
reviewNote: null,
archivedBy: null,
archivedAt: null,
archiveNote: null,
uploadedBy: me.id,
createdAt: "2026-05-06T09:20:00.000Z",
uploader: { id: me.id, employeeNo: me.employeeNo, name: me.name },
@@ -314,6 +387,25 @@ async function mockApis(page: Page) {
return;
}
if (
pathName === `/api/purchase-requests/${purchaseId}/attachments/${safePurchaseAttachmentId}/download` &&
request.method() === "POST"
) {
expect(request.headers().authorization).toBe("Bearer mock-r103-token");
expect(request.headers()["content-type"]).toContain("application/json");
expect(request.postDataJSON()).toEqual({ reason: "采购复核" });
await route.fulfill({
status: 200,
headers: {
"content-type": "application/octet-stream",
"content-disposition": 'attachment; filename="purchase-attachment.png"',
"x-content-type-options": "nosniff",
},
body: safePngBuffer,
});
return;
}
if (pathName === "/api/invoice-pool") {
await fulfillJson(route, { page: 1, pageSize: 20, total: 1, ownerScope: "own", items: [invoiceItem()] });
return;
@@ -342,7 +434,7 @@ test.describe("R103 cross page UI consistency", () => {
await login(page);
});
test("采购页显示中文标题并使用中文附件上传控件", async ({ page }) => {
test("采购页使用安全格式上传并通过认证 Blob 下载", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 960 });
await page.goto("/purchase-requests");
@@ -361,14 +453,44 @@ test.describe("R103 cross page UI consistency", () => {
await expect(page.getByTestId("purchase-requests-table")).toHaveCount(0);
await expect(page.getByTestId("purchase-advanced-filters")).toHaveCount(0);
await expect(page.getByTestId("purchase-attachment-upload")).toHaveText("上传附件");
await expect(page.getByText("支持 PDF、JPG、PNG、WEBP,下载会留痕,OA 内不直接预览。")).toBeVisible();
await expect(page.getByText("Choose File")).toHaveCount(0);
await page.getByTestId("purchase-attachment-file-input").setInputFiles({
name: "采购报价.pdf",
mimeType: "application/pdf",
buffer: Buffer.from("r103"),
const attachmentInput = page.getByTestId("purchase-attachment-file-input");
await expect(attachmentInput).toHaveAttribute(
"accept",
".pdf,.jpg,.jpeg,.png,.webp,application/pdf,image/jpeg,image/png,image/webp",
);
await attachmentInput.setInputFiles({
name: "采购报价.png",
mimeType: "image/png",
buffer: safePngBuffer,
});
await expect(page.getByTestId("purchase-attachment-selected-name")).toContainText("采购报价.pdf");
await expect(page.getByTestId("purchase-attachment-selected-name")).toContainText("采购报价.png");
await attachmentInput.setInputFiles({
name: "unsupported.txt",
mimeType: "text/plain",
buffer: Buffer.from("synthetic unsupported attachment", "utf8"),
});
await expect(page.getByRole("main").getByText("仅支持 PDF、JPG、PNG、WEBP 文件,请重新选择。")).toBeVisible();
let popupOpened = false;
page.on("popup", () => {
popupOpened = true;
});
await page.getByTestId(`purchase-attachment-download-desktop-${safePurchaseAttachmentId}`).click();
await expect(page.getByRole("heading", { name: "填写下载用途" })).toBeVisible();
await page.getByRole("textbox", { name: "下载用途" }).fill("采购复核");
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "继续下载" }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe("采购报价.png");
expect(popupOpened).toBe(false);
await expect(
page.getByTestId(`purchase-attachment-download-desktop-${quarantinedPurchaseAttachmentId}`),
).toBeDisabled();
await page.screenshot({ path: screenshotPath("purchase-desktop.png"), fullPage: true });
});