diff --git a/e2e/document-renderer.playwright.ts b/e2e/document-renderer.playwright.ts index 45d987e..01bfb0b 100644 --- a/e2e/document-renderer.playwright.ts +++ b/e2e/document-renderer.playwright.ts @@ -550,7 +550,7 @@ test('§11-8 无权用户的页面与网络响应不含受限内容', async ({ b await page.waitForTimeout(1_500) await shot(page, 'item8-outsider-audit') const pageText = (await page.locator('body').textContent()) ?? '' - for (const m of [A.name, A.title, ANNOTATION, REVISION_AFTER]) expect(pageText, `页面不得出现受限内容「${m}」`).not.toContain(m) + for (const m of [A.name, A.title, UNCTRL.title, ANNOTATION, REVISION_AFTER]) expect(pageText, `页面不得出现受限内容「${m}」`).not.toContain(m) const passiveCount = s.net.length // 主动探测:以无权用户会话直调 A 的读取与下载命令(请求中自带的 id 不算泄露) @@ -565,8 +565,10 @@ test('§11-8 无权用户的页面与网络响应不含受限内容', async ({ b ...(st.submissionId ? [['rdm.render.review.view', { projectId: A.pid, submissionId: st.submissionId }] as [string, Record]] : []), ] const probeResults: { id: string; status: number; state: string; hasPayload: boolean }[] = [] + const probeBodies: { url: string; body: string }[] = [] for (const [id, params] of probes) { const r = await command(page, id, params) + probeBodies.push({ url: `probe:${id}`, body: r.text }) const state = r.body?.receipt?.state ?? '' const payload = r.body?.payload const hasPayload = payload !== undefined && payload !== null && !(typeof payload === 'object' && Object.keys(payload).length === 0) @@ -576,6 +578,7 @@ test('§11-8 无权用户的页面与网络响应不含受限内容', async ({ b } for (const kind of ['markdown', 'docx', 'manifest']) { const r = await command(page, 'rdm.render.download', { projectId: A.pid, deliveryId: 'probe', kind }) + probeBodies.push({ url: `probe:rdm.render.download:${kind}`, body: r.text }) probeResults.push({ id: `rdm.render.download:${kind}`, status: r.status, state: r.body?.receipt?.state ?? '', hasPayload: !!r.body?.payload }) expect.soft(!r.body?.payload?.dataBase64, 'rdm.render.download 不得返回文件内容').toBe(true) } @@ -583,22 +586,25 @@ test('§11-8 无权用户的页面与网络响应不含受限内容', async ({ b const hostText = await hostProjects.text() expect.soft(hostText.includes(A.host), '宿主项目列表不得含项目 A').toBe(false) const hostConv = await page.request.get(`/api/projects/${A.host}/conversations`) + const hostConvText = await hostConv.text() + probeBodies.push({ url: '/api/projects', body: hostText }, { url: `/api/projects/${A.host}/conversations`, body: hostConvText }) note(`宿主 A 会话列表 HTTP ${hostConv.status()}`) - expect.soft(hostConv.status() >= 400 || !(await hostConv.text()).includes('§11'), '宿主 A 会话不得可见').toBe(true) + expect.soft(hostConv.status() >= 400 || !hostConvText.includes('§11'), '宿主 A 会话不得可见').toBe(true) note(`主动探测:${JSON.stringify(probeResults)}`) await Promise.all(s.pending) // 全部网络响应体扫描:内容标记任何响应都不得出现;A 的标识只在被动浏览响应中检查 - const contentMarkers = [A.name, A.title, ANNOTATION, REVISION_AFTER, ...(st.downloads ?? [])] + const contentMarkers = [A.name, A.title, UNCTRL.title, ANNOTATION, REVISION_AFTER, ...(st.downloads ?? [])] const idMarkers = [A.pid, A.host, st.submissionId].filter(Boolean) const leaks: string[] = [] s.net.forEach((n, i) => { for (const m of contentMarkers) if (m && n.body.includes(m)) leaks.push(`${n.url} 含「${m}」`) if (i < passiveCount) for (const m of idMarkers) if (n.body.includes(m)) leaks.push(`${n.url} 含标识 ${m}`) }) + for (const p of probeBodies) for (const m of contentMarkers) if (m && p.body.includes(m)) leaks.push(`${p.url} 含「${m}」`) const scanned = path.join(EVID, 'network', 'item8-scan.json') fs.mkdirSync(path.dirname(scanned), { recursive: true }) - fs.writeFileSync(scanned, JSON.stringify({ responses: s.net.length, passive: passiveCount, contentMarkers, idMarkers, leaks, probeResults }, null, 2)) + fs.writeFileSync(scanned, JSON.stringify({ pageResponses: s.net.length, passive: passiveCount, probeResponses: probeBodies.length, contentMarkers, idMarkers, leaks, probeResults }, null, 2)) evidence.push(scanned) expect(leaks, `受限内容泄露:${leaks.join(';')}`).toEqual([]) await s.close()