fix: adversarial review fixes for ux-audit and heatmap

Security:
- Remove live form value extraction from ux-audit (leaked input field values)
- Add ux-audit to PAGE_CONTENT_COMMANDS (untrusted content wrapping)

Correctness:
- Scope youAreHere selector to nav containers (was matching animation classes)
- Validate heatmap JSON is a plain object (string/array/null produced garbage)
- Use textContent instead of innerText for word count (avoids layout computation)
- Remove dead url variable and unused LINK_CAP constant

Found by Codex + Claude adversarial review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-14 09:08:57 -07:00
parent c12c30e191
commit e2ab41502e
3 changed files with 13 additions and 8 deletions
+1
View File
@@ -50,6 +50,7 @@ export const PAGE_CONTENT_COMMANDS = new Set([
'text', 'html', 'links', 'forms', 'accessibility', 'attrs',
'console', 'dialog',
'media', 'data',
'ux-audit',
]);
/** Wrap output from untrusted-content commands with trust boundary markers */
+6 -6
View File
@@ -656,13 +656,12 @@ export async function handleMetaCommand(
// ─── UX Audit ─────────────────────────────────────
case 'ux-audit': {
const page = bm.getPage();
const url = page.url();
// Extract page structure for UX behavioral analysis
// Agent interprets the data and applies Krug's 6 usability tests
// Uses textContent (not innerText) to avoid layout computation on large DOMs
const data = await page.evaluate(() => {
const HEADING_CAP = 50;
const LINK_CAP = 100;
const INTERACTIVE_CAP = 200;
const TEXT_BLOCK_CAP = 50;
@@ -695,7 +694,8 @@ export async function handleMetaCommand(
});
// "You are here" indicator: current/active nav items
const activeNavItems = document.querySelectorAll('[aria-current], .active, .current, [class*="active"], [class*="current"]');
// Scoped to nav containers to avoid false positives from animation classes
const activeNavItems = document.querySelectorAll('nav [aria-current], nav .active, nav .current, [role="navigation"] [aria-current], [role="navigation"] .active, [role="navigation"] .current');
const youAreHere = Array.from(activeNavItems).slice(0, 5).map(el => ({
text: (el.textContent || '').trim().slice(0, 50),
tag: el.tagName,
@@ -725,7 +725,7 @@ export async function handleMetaCommand(
const rect = el.getBoundingClientRect();
return {
tag: el.tagName,
text: (el.textContent || (el as HTMLInputElement).placeholder || (el as HTMLInputElement).value || '').trim().slice(0, 50),
text: (el.textContent || (el as HTMLInputElement).placeholder || '').trim().slice(0, 50),
type: (el as HTMLInputElement).type || null,
role: el.getAttribute('role'),
w: Math.round(rect.width),
@@ -740,8 +740,8 @@ export async function handleMetaCommand(
wordCount: (el.textContent || '').trim().split(/\s+/).filter(Boolean).length,
}));
// Total visible text word count
const bodyText = (document.body?.innerText || '').trim();
// Total visible text word count (textContent avoids layout computation)
const bodyText = (document.body?.textContent || '').trim();
const totalWords = bodyText.split(/\s+/).filter(Boolean).length;
return {
+6 -2
View File
@@ -482,9 +482,13 @@ export async function handleSnapshot(
let colorAssignments: Record<string, string>;
try {
colorAssignments = JSON.parse(opts.heatmap);
const parsed = JSON.parse(opts.heatmap);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('not an object');
}
colorAssignments = parsed;
} catch {
throw new Error('Invalid heatmap JSON. Expected: \'{"@e1":"green","@e3":"red"}\'');
throw new Error('Invalid heatmap JSON. Expected object: \'{"@e1":"green","@e3":"red"}\'');
}
// Validate colors