From 701ec8fb880a8bcd79521e94d7440e6cbd9f0eca Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Wed, 29 Apr 2026 14:20:23 +0200 Subject: [PATCH] port(pi-mono): escape session metadata + image data in HTML export (refs 7617c1ad9, 57787b655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pi-mono Tier 0 #1 (security) — sf-driven port. Two upstream security fixes (pi-mono PR #3819, #3883) that escape user-controlled session content before embedding in HTML exports. Crafted session content (image mime types, image data, model IDs, tool names, entry IDs) could otherwise inject markup at the export boundary. What sf changed in packages/pi-coding-agent/src/core/export-html/template.js: - Image tags: escape `mimeType` and `data` attributes for both tool-result and user-message image renders (PR #3819). - Session metadata: escape `msg.toolName`, `msg.role`, `entry.modelId`, `entry.thinkingLevel`, `entry.type`, `entry.id`, and `globalStats.models` (PR #3883). - DOM id construction: renamed `entryId` → `entryDomId` and escape `entry.id` to prevent attribute-breakout from a crafted id. The existing `escapeHtml()` helper was used at every site; no new helper introduced. Type-check passes. Co-Authored-By: sf v2.75.1 (session 150fe2c1) Co-Authored-By: Claude Sonnet 4.6 --- .../src/core/export-html/template.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/pi-coding-agent/src/core/export-html/template.js b/packages/pi-coding-agent/src/core/export-html/template.js index e170d7a6b..85117bdbf 100644 --- a/packages/pi-coding-agent/src/core/export-html/template.js +++ b/packages/pi-coding-agent/src/core/export-html/template.js @@ -634,13 +634,13 @@ if (toolCall) { return labelHtml + `${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}`; } - return labelHtml + `[${msg.toolName || 'tool'}]`; + return labelHtml + `[${escapeHtml(msg.toolName || 'tool')}]`; } if (msg.role === 'bashExecution') { const cmd = truncate(normalize(msg.command || '')); return labelHtml + `[bash]: ${escapeHtml(cmd)}`; } - return labelHtml + `[${msg.role}]`; + return labelHtml + `[${escapeHtml(msg.role)}]`; } case 'compaction': return labelHtml + `[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]`; @@ -653,11 +653,11 @@ return labelHtml + `[${escapeHtml(entry.customType)}]: ${escapeHtml(truncate(normalize(content)))}`; } case 'model_change': - return labelHtml + `[model: ${entry.modelId}]`; + return labelHtml + `[model: ${escapeHtml(entry.modelId)}]`; case 'thinking_level_change': - return labelHtml + `[thinking: ${entry.thinkingLevel}]`; + return labelHtml + `[thinking: ${escapeHtml(entry.thinkingLevel)}]`; default: - return labelHtml + `[${entry.type}]`; + return labelHtml + `[${escapeHtml(entry.type)}]`; } } @@ -880,7 +880,7 @@ const images = getResultImages(); if (images.length === 0) return ''; return '
' + - images.map(img => ``).join('') + + images.map(img => ``).join('') + '
'; }; @@ -1105,7 +1105,7 @@ * Render the copy-link button HTML for a message. */ function renderCopyLinkButton(entryId) { - return `