port(pi-mono): escape session metadata + image data in HTML export (refs 7617c1ad9, 57787b655)
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 <noreply@anthropic.com>
This commit is contained in:
parent
7c487bb60e
commit
701ec8fb88
1 changed files with 17 additions and 17 deletions
|
|
@ -634,13 +634,13 @@
|
||||||
if (toolCall) {
|
if (toolCall) {
|
||||||
return labelHtml + `<span class="tree-role-tool">${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}</span>`;
|
return labelHtml + `<span class="tree-role-tool">${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}</span>`;
|
||||||
}
|
}
|
||||||
return labelHtml + `<span class="tree-role-tool">[${msg.toolName || 'tool'}]</span>`;
|
return labelHtml + `<span class="tree-role-tool">[${escapeHtml(msg.toolName || 'tool')}]</span>`;
|
||||||
}
|
}
|
||||||
if (msg.role === 'bashExecution') {
|
if (msg.role === 'bashExecution') {
|
||||||
const cmd = truncate(normalize(msg.command || ''));
|
const cmd = truncate(normalize(msg.command || ''));
|
||||||
return labelHtml + `<span class="tree-role-tool">[bash]:</span> ${escapeHtml(cmd)}`;
|
return labelHtml + `<span class="tree-role-tool">[bash]:</span> ${escapeHtml(cmd)}`;
|
||||||
}
|
}
|
||||||
return labelHtml + `<span class="tree-muted">[${msg.role}]</span>`;
|
return labelHtml + `<span class="tree-muted">[${escapeHtml(msg.role)}]</span>`;
|
||||||
}
|
}
|
||||||
case 'compaction':
|
case 'compaction':
|
||||||
return labelHtml + `<span class="tree-compaction">[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]</span>`;
|
return labelHtml + `<span class="tree-compaction">[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]</span>`;
|
||||||
|
|
@ -653,11 +653,11 @@
|
||||||
return labelHtml + `<span class="tree-custom">[${escapeHtml(entry.customType)}]:</span> ${escapeHtml(truncate(normalize(content)))}`;
|
return labelHtml + `<span class="tree-custom">[${escapeHtml(entry.customType)}]:</span> ${escapeHtml(truncate(normalize(content)))}`;
|
||||||
}
|
}
|
||||||
case 'model_change':
|
case 'model_change':
|
||||||
return labelHtml + `<span class="tree-muted">[model: ${entry.modelId}]</span>`;
|
return labelHtml + `<span class="tree-muted">[model: ${escapeHtml(entry.modelId)}]</span>`;
|
||||||
case 'thinking_level_change':
|
case 'thinking_level_change':
|
||||||
return labelHtml + `<span class="tree-muted">[thinking: ${entry.thinkingLevel}]</span>`;
|
return labelHtml + `<span class="tree-muted">[thinking: ${escapeHtml(entry.thinkingLevel)}]</span>`;
|
||||||
default:
|
default:
|
||||||
return labelHtml + `<span class="tree-muted">[${entry.type}]</span>`;
|
return labelHtml + `<span class="tree-muted">[${escapeHtml(entry.type)}]</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -880,7 +880,7 @@
|
||||||
const images = getResultImages();
|
const images = getResultImages();
|
||||||
if (images.length === 0) return '';
|
if (images.length === 0) return '';
|
||||||
return '<div class="tool-images">' +
|
return '<div class="tool-images">' +
|
||||||
images.map(img => `<img src="data:${img.mimeType};base64,${img.data}" class="tool-image" />`).join('') +
|
images.map(img => `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="tool-image" />`).join('') +
|
||||||
'</div>';
|
'</div>';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1105,7 +1105,7 @@
|
||||||
* Render the copy-link button HTML for a message.
|
* Render the copy-link button HTML for a message.
|
||||||
*/
|
*/
|
||||||
function renderCopyLinkButton(entryId) {
|
function renderCopyLinkButton(entryId) {
|
||||||
return `<button class="copy-link-btn" data-entry-id="${entryId}" title="Copy link to this message">
|
return `<button class="copy-link-btn" data-entry-id="${escapeHtml(entryId)}" title="Copy link to this message">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||||
|
|
@ -1116,14 +1116,14 @@
|
||||||
function renderEntry(entry) {
|
function renderEntry(entry) {
|
||||||
const ts = formatTimestamp(entry.timestamp);
|
const ts = formatTimestamp(entry.timestamp);
|
||||||
const tsHtml = ts ? `<div class="message-timestamp">${ts}</div>` : '';
|
const tsHtml = ts ? `<div class="message-timestamp">${ts}</div>` : '';
|
||||||
const entryId = `entry-${entry.id}`;
|
const entryDomId = `entry-${escapeHtml(entry.id)}`;
|
||||||
const copyBtnHtml = renderCopyLinkButton(entry.id);
|
const copyBtnHtml = renderCopyLinkButton(entry.id);
|
||||||
|
|
||||||
if (entry.type === 'message') {
|
if (entry.type === 'message') {
|
||||||
const msg = entry.message;
|
const msg = entry.message;
|
||||||
|
|
||||||
if (msg.role === 'user') {
|
if (msg.role === 'user') {
|
||||||
let html = `<div class="user-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||||
const content = msg.content;
|
const content = msg.content;
|
||||||
|
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
|
|
@ -1131,7 +1131,7 @@
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
html += '<div class="message-images">';
|
html += '<div class="message-images">';
|
||||||
for (const img of images) {
|
for (const img of images) {
|
||||||
html += `<img src="data:${img.mimeType};base64,${img.data}" class="message-image" />`;
|
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
@ -1147,7 +1147,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.role === 'assistant') {
|
if (msg.role === 'assistant') {
|
||||||
let html = `<div class="assistant-message" id="${entryId}">${copyBtnHtml}${tsHtml}`;
|
let html = `<div class="assistant-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||||
|
|
||||||
for (const block of msg.content) {
|
for (const block of msg.content) {
|
||||||
if (block.type === 'text' && block.text.trim()) {
|
if (block.type === 'text' && block.text.trim()) {
|
||||||
|
|
@ -1178,7 +1178,7 @@
|
||||||
|
|
||||||
if (msg.role === 'bashExecution') {
|
if (msg.role === 'bashExecution') {
|
||||||
const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null);
|
const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null);
|
||||||
let html = `<div class="tool-execution ${isError ? 'error' : 'success'}" id="${entryId}">${tsHtml}`;
|
let html = `<div class="tool-execution ${isError ? 'error' : 'success'}" id="${entryDomId}">${tsHtml}`;
|
||||||
html += `<div class="tool-command">$ ${escapeHtml(msg.command)}</div>`;
|
html += `<div class="tool-command">$ ${escapeHtml(msg.command)}</div>`;
|
||||||
if (msg.output) html += formatExpandableOutput(msg.output, 10);
|
if (msg.output) html += formatExpandableOutput(msg.output, 10);
|
||||||
if (msg.cancelled) {
|
if (msg.cancelled) {
|
||||||
|
|
@ -1194,11 +1194,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'model_change') {
|
if (entry.type === 'model_change') {
|
||||||
return `<div class="model-change" id="${entryId}">${tsHtml}Switched to model: <span class="model-name">${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}</span></div>`;
|
return `<div class="model-change" id="${entryDomId}">${tsHtml}Switched to model: <span class="model-name">${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'compaction') {
|
if (entry.type === 'compaction') {
|
||||||
return `<div class="compaction" id="${entryId}" onclick="this.classList.toggle('expanded')">
|
return `<div class="compaction" id="${entryDomId}" onclick="this.classList.toggle('expanded')">
|
||||||
<div class="compaction-label">[compaction]</div>
|
<div class="compaction-label">[compaction]</div>
|
||||||
<div class="compaction-collapsed">Compacted from ${entry.tokensBefore.toLocaleString()} tokens</div>
|
<div class="compaction-collapsed">Compacted from ${entry.tokensBefore.toLocaleString()} tokens</div>
|
||||||
<div class="compaction-content"><strong>Compacted from ${entry.tokensBefore.toLocaleString()} tokens</strong>\n\n${escapeHtml(entry.summary)}</div>
|
<div class="compaction-content"><strong>Compacted from ${entry.tokensBefore.toLocaleString()} tokens</strong>\n\n${escapeHtml(entry.summary)}</div>
|
||||||
|
|
@ -1206,14 +1206,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'branch_summary') {
|
if (entry.type === 'branch_summary') {
|
||||||
return `<div class="branch-summary" id="${entryId}">${tsHtml}
|
return `<div class="branch-summary" id="${entryDomId}">${tsHtml}
|
||||||
<div class="branch-summary-header">Branch Summary</div>
|
<div class="branch-summary-header">Branch Summary</div>
|
||||||
<div class="markdown-content">${safeMarkedParse(entry.summary)}</div>
|
<div class="markdown-content">${safeMarkedParse(entry.summary)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.type === 'custom_message' && entry.display) {
|
if (entry.type === 'custom_message' && entry.display) {
|
||||||
return `<div class="hook-message" id="${entryId}">${tsHtml}
|
return `<div class="hook-message" id="${entryDomId}">${tsHtml}
|
||||||
<div class="hook-type">[${escapeHtml(entry.customType)}]</div>
|
<div class="hook-type">[${escapeHtml(entry.customType)}]</div>
|
||||||
<div class="markdown-content">${safeMarkedParse(typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content))}</div>
|
<div class="markdown-content">${safeMarkedParse(typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content))}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
@ -1295,7 +1295,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}</span></div>
|
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}</span></div>
|
||||||
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">${globalStats.models.join(', ') || 'unknown'}</span></div>
|
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">${escapeHtml(globalStats.models.join(', ') || 'unknown')}</span></div>
|
||||||
<div class="info-item"><span class="info-label">Messages:</span><span class="info-value">${msgParts.join(', ') || '0'}</span></div>
|
<div class="info-item"><span class="info-label">Messages:</span><span class="info-value">${msgParts.join(', ') || '0'}</span></div>
|
||||||
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">${globalStats.toolCalls}</span></div>
|
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">${globalStats.toolCalls}</span></div>
|
||||||
<div class="info-item"><span class="info-label">Tokens:</span><span class="info-value">${tokenParts.join(' ') || '0'}</span></div>
|
<div class="info-item"><span class="info-label">Tokens:</span><span class="info-value">${tokenParts.join(' ') || '0'}</span></div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue