/** * Phase 2 adversarial — UI-side defenses. Confirms that Svelte's default * text interpolation escapes everywhere user-supplied content surfaces. * * This is a belt-and-braces test: Svelte 5 escapes `{value}` by default, * so failures here would mean someone reached for `{@html}` somewhere * they shouldn't. */ import { test, expect } from '../../fixtures/test'; test.describe('Adversarial — UI render escape', () => { test('display name with BOLD`; const r = await api.join(payload); await page.goto('/'); await page.evaluate(({ j, u, n, p }) => { localStorage.setItem('eventsnap_jwt', j); localStorage.setItem('eventsnap_user_id', u); localStorage.setItem('eventsnap_display_name', n); localStorage.setItem('eventsnap_pin', p); }, { j: r.jwt, u: r.user_id, n: payload, p: r.pin }); page.on('dialog', (d) => { throw new Error(`Dialog fired: ${d.message()}`); }); await page.goto('/account'); // Render guard FIRST. `domcontentloaded` fires before Svelte hydrates, so asserting // the *absence* of a at that point passes on a page that never rendered the name // at all — a false green on an XSS test. Prove the payload actually reached the DOM // (as escaped text) before concluding anything about how it was rendered. await expect(page.getByText(payload, { exact: false }).first()).toBeVisible({ timeout: 10_000 }); const fired = await page.evaluate(() => (window as any).__xssFired === true); expect(fired).toBe(false); // tag inside the name should also not render as bold — Svelte escapes the entire // string. toHaveCount auto-retries, so this can't win by racing hydration. await expect(page.locator('b:has-text("BOLD")')).toHaveCount(0); await expect(page.locator('script:has-text("__xssFired")')).toHaveCount(0); }); test('rendering of a known SQL-injection-shaped name does not break the page', async ({ page, api }) => { const payload = `'); DROP TABLE users; --`; const r = await api.join(payload); await page.goto('/'); await page.evaluate(({ j, u, n, p }) => { localStorage.setItem('eventsnap_jwt', j); localStorage.setItem('eventsnap_user_id', u); localStorage.setItem('eventsnap_display_name', n); localStorage.setItem('eventsnap_pin', p); }, { j: r.jwt, u: r.user_id, n: payload, p: r.pin }); await page.goto('/account'); // Page renders. await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible(); }); });