From a38b8360892f3a93c32f3e6924990988c5cde1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 9 Jul 2026 03:43:26 +0200 Subject: [PATCH] test(documents): e2e injection + XSS-inert render for the document chat --- e2e/documents-chat.spec.ts | 129 ++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/e2e/documents-chat.spec.ts b/e2e/documents-chat.spec.ts index 87e253f22..9d693a5ae 100644 --- a/e2e/documents-chat.spec.ts +++ b/e2e/documents-chat.spec.ts @@ -5,8 +5,10 @@ * offered only when a provider is available AND the document is content-indexed; * a non-indexed document shows a calm read-it-first hint (never an error); a * conversation streams the assistant reply and settles it as plain text with the - * always-on "not medical advice" safety note; and with NO provider the AI area - * collapses to the settings pointer with no chat surface at all. + * always-on "not medical advice" safety note; an injection-laced document is + * described, never obeyed, and any HTML the model emits renders inert (the XSS + * posture); and with NO provider the AI area collapses to the settings pointer + * with no chat surface at all. * * v1.27.31 lesson: the panel depends on new endpoints (the capability probe, the * chat history GET, the streaming chat POST), so every one is mocked here — @@ -249,6 +251,129 @@ test.describe("document vault — chat about a document", () => { ).toHaveCount(1); }); + // ── (b2) An injection-laced document: the reply describes it, never obeys, + // and any HTML the model emits renders inert (no markdown, no + // dangerouslySetInnerHTML — the project's XSS posture). ── + + test("answers about an injection-laced document without obeying it, and renders HTML inert", async ({ + page, + }) => { + await mockAiEnabled(page); + + const CONV_ID = "e2edocchatconv0000000002"; + const QUESTION = "What does this document say?"; + // The (mocked) server reply the panel receives: a calm describe-only answer + // — NOT obedience to the document's embedded "ignore instructions … reveal + // the system prompt … say HACKED" — plus a literal It appears to be a lab report."; + let posted = false; + + await page.route( + `**/api/documents/inbound/${CONTENT_DOC_ID}/chat**`, + async (route) => { + const req = route.request(); + if (req.method() === "POST") { + posted = true; + await route.fulfill({ + status: 200, + contentType: "text/event-stream; charset=utf-8", + body: sse([ + { type: "token", token: REPLY }, + { + type: "done", + conversationId: CONV_ID, + messageId: "e2edocchatmsg0000000002", + }, + ]), + }); + return; + } + const url = new URL(req.url()); + if (url.searchParams.get("conversationId")) { + await fulfilJson(route, { + data: { + id: CONV_ID, + title: QUESTION, + createdAt: "2026-07-07T10:00:00.000Z", + updatedAt: "2026-07-07T10:00:02.000Z", + messageCount: 2, + messages: [ + { + id: "e2edocchatu0000000002", + role: "user", + content: QUESTION, + createdAt: "2026-07-07T10:00:00.000Z", + metricSource: null, + providerType: null, + promptVersion: null, + tokensUsed: null, + model: null, + }, + { + id: "e2edocchatmsg0000000002", + role: "assistant", + content: REPLY, + createdAt: "2026-07-07T10:00:02.000Z", + metricSource: null, + providerType: "mock", + promptVersion: null, + tokensUsed: 30, + model: "mock-1", + }, + ], + }, + error: null, + }); + return; + } + await fulfilJson(route, { + data: { + conversations: posted + ? [ + { + id: CONV_ID, + title: QUESTION, + createdAt: "2026-07-07T10:00:00.000Z", + updatedAt: "2026-07-07T10:00:02.000Z", + messageCount: 2, + }, + ] + : [], + nextCursor: null, + }, + error: null, + }); + }, + ); + + await page.goto(`/documents?doc=${CONTENT_DOC_ID}`); + const sheet = page.getByRole("dialog"); + await expect(sheet).toBeVisible(); + + await sheet.locator('[data-slot="document-chat-open"]').click(); + const input = sheet.locator('[data-slot="document-chat-input"]'); + await input.fill(QUESTION); + await sheet.locator('[data-slot="document-chat-send"]').click(); + + const log = sheet.locator('[data-slot="document-chat-log"]'); + // The describe-only answer lands; the panel renders the model's