diff --git a/packages/join-block/src/Settings.php b/packages/join-block/src/Settings.php index 78520a7..047c5c6 100644 --- a/packages/join-block/src/Settings.php +++ b/packages/join-block/src/Settings.php @@ -290,7 +290,9 @@ public static function init() if (!$log) { $log = 'Could not load error log. Please contact Common Knowledge support.'; } - return "
$log
"; + // The log holds text sent by visitors (join and step requests), so + // it must be shown as text, never as markup. + return '
' . esc_html($log) . '
'; }); /** @var Html_Field $logRetentionField */ $logRetentionField = Field::make('html', 'ck_join_flow_log_retention'); diff --git a/packages/join-e2e/tests/log-viewer.spec.ts b/packages/join-e2e/tests/log-viewer.spec.ts new file mode 100644 index 0000000..6fec05d --- /dev/null +++ b/packages/join-e2e/tests/log-viewer.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; +import { loginAsAdmin } from './helpers'; + +/** + * The plugin's log, shown on WP Admin > Join > Logging, holds text that + * visitors send. It must be shown as text: markup in it must never run in the + * admin's browser. + */ +test('markup sent by a visitor is shown as text in the log viewer, not run', async ({ page, request }) => { + const probe = `log-viewer-probe-${Date.now()}`; + const markup = ``; + + // A join on a plan that doesn't exist logs the plan's ID as sent. + const response = await request.post('/wp-json/join/v1/join', { + data: { email: 'someone@example.com', membership: markup }, + }); + expect(response.status()).toBe(500); + + await loginAsAdmin(page); + await page.goto('/wp-admin/admin.php?page=crb_carbon_fields_container_ck_join_flow.php'); + await page.getByRole('tab', { name: 'Logging', exact: true }).click(); + + await expect(page.locator('pre', { hasText: `Invalid membership plan: ${markup}` })).toBeVisible(); + await expect(page.locator(`#${probe}`)).toHaveCount(0); + expect(await page.evaluate((name) => (window as any)[name], probe.replace(/-/g, '_'))).toBeUndefined(); +});