import { test, expect } from '@playwright/test'; import { FRONTEND_URL, createTestAddress, seedTestMail, deleteAddress, deleteAllMailpitMessages, requestSendAccess, } from '../../fixtures/test-helpers'; import { request as apiRequest } from '@playwright/test'; test.describe('Reply HTML & XSS Sanitization', () => { test('reply to HTML email — XSS payloads stripped, HTML preserved', async ({ page }) => { const api = await apiRequest.newContext(); let jwt: string | undefined; try { await deleteAllMailpitMessages(api); const created = await createTestAddress(api, 'reply-xss'); jwt = created.jwt; const address = created.address; // Request send access so Reply can navigate to compose form await requestSendAccess(api, jwt); // Seed email with XSS payloads embedded in HTML const xssHtml = [ '
', '

Important Message

', '

Please review this content.

', ' ', ' ', ' click me', '

Styled paragraph

', '
', ].join('\n'); await seedTestMail(api, address, { subject: 'XSS Test Email', html: xssHtml, from: 'attacker@test.example.com', }); // Single dialog handler with phase tracking. // During email rendering, the mail viewer uses an unsandboxed iframe so // inline event handlers like onerror may fire — we dismiss those. // After clicking Reply, any dialog means the compose path failed to sanitize. let inComposePhase = false; let composeDialogAppeared = false; page.on('dialog', async (dialog) => { if (inComposePhase) composeDialogAppeared = true; await dialog.dismiss(); }); // Login with English locale await page.goto(`${FRONTEND_URL}/en/?jwt=${jwt}`); // Open the email (use listitem to avoid strict mode violation // when detail panel also shows the subject) const mailItem = page.getByRole('listitem').getByText('XSS Test Email'); await expect(mailItem).toBeVisible({ timeout: 10_000 }); await mailItem.click(); // Wait for Reply button to appear — signals email content has rendered const replyButton = page.locator('button').filter({ hasText: /Reply/i }).first(); await expect(replyButton).toBeVisible({ timeout: 10_000 }); // Click Reply — from here on, dialogs indicate sanitization failure (#857) inComposePhase = true; await replyButton.click(); // In the reply compose area, check that the forwarded HTML is sanitized: // -