fix(electrobun): address all 22 Codex review #2 findings

CRITICAL:
- DocsTab XSS: DOMPurify sanitization on all {@html} output
- File RPC path traversal: guardPath() validates against project CWDs

HIGH:
- SSH injection: spawn /usr/bin/ssh via PTY args, no shell string
- Search XSS: strip HTML, highlight matches client-side with <mark>
- Terminal listener leak: cleanup functions stored + called in onDestroy
- FileBrowser race: request token, discard stale responses
- SearchOverlay race: same request token pattern
- App startup ordering: groups.list chains into active_group restore
- PtyClient timeout: 5-second auth timeout on connect()
- Rule 55: 6 {#if} patterns converted to style:display toggle

MEDIUM:
- Agent persistence: only persist NEW messages (lastPersistedIndex)
- Search errors: typed error response, "Invalid query" UI
- Health store wired: agent events call recordActivity/setProjectStatus
- index.ts SRP: split into 8 domain handler modules (298 lines)
- App.svelte: extracted workspace-store.svelte.ts
- rpc.ts: typed AppRpcHandle, removed `any`

LOW:
- CommandPalette listener wired in App.svelte
- Dead code removed (removeGroup, onDragStart, plugin loaded)
This commit is contained in:
Hibryda 2026-03-22 02:30:09 +01:00
parent 8e756d3523
commit 1cd4558740
28 changed files with 1342 additions and 1164 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import DOMPurify from 'dompurify';
import { appRpc } from './rpc.ts';
interface Props {
@ -19,6 +20,12 @@
let renderedHtml = $state('');
let loading = $state(false);
// Fix #1: Configure DOMPurify with safe tag whitelist
const PURIFY_CONFIG = {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'p', 'a', 'strong', 'em', 'code', 'pre', 'ul', 'li', 'br'],
ALLOWED_ATTR: ['href', 'target', 'class'],
};
function expandHome(p: string): string {
if (p.startsWith('~/')) return p.replace('~', '/home/' + (typeof process !== 'undefined' ? process.env.USER : 'user'));
return p;
@ -44,19 +51,26 @@
const res = await appRpc.request['files.read']({ path: file.path });
if (res.error || !res.content) {
content = '';
renderedHtml = `<p class="doc-error">${res.error ?? 'Empty file'}</p>`;
renderedHtml = DOMPurify.sanitize(
`<p class="doc-error">${escapeHtml(res.error ?? 'Empty file')}</p>`,
PURIFY_CONFIG,
);
} else {
content = res.content;
renderedHtml = renderMarkdown(content);
renderedHtml = DOMPurify.sanitize(renderMarkdown(content), PURIFY_CONFIG);
}
} catch (err) {
console.error('[DocsTab] read error:', err);
renderedHtml = '<p class="doc-error">Failed to read file</p>';
renderedHtml = DOMPurify.sanitize('<p class="doc-error">Failed to read file</p>', PURIFY_CONFIG);
}
loading = false;
}
/** Simple markdown-to-HTML (no external dep). Handles headers, code blocks, bold, italic, links, lists. */
function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/** Simple markdown-to-HTML. All output is sanitized by DOMPurify before rendering. */
function renderMarkdown(md: string): string {
let html = md
.replace(/&/g, '&amp;')
@ -64,8 +78,8 @@
.replace(/>/g, '&gt;');
// Code blocks
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) =>
`<pre class="doc-code"><code class="lang-${lang}">${code.trim()}</code></pre>`
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) =>
`<pre class="doc-code"><code>${code.trim()}</code></pre>`
);
// Inline code
html = html.replace(/`([^`]+)`/g, '<code class="doc-inline-code">$1</code>');