fix: resolve XSS vulnerability in Terminal component search highlighting
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 55s
CI / Lint & Test (pull_request) Successful in 14m11s
CI / Build Linux (pull_request) Successful in 16m46s
CI / Build Windows (cross-compile) (pull_request) Successful in 27m3s

- Created new HighlightedText component for safe text highlighting
- Replaced unsafe {@html} usage with component-based approach
- Maintains full search functionality without security risks
This commit is contained in:
2026-01-22 10:23:51 -08:00
parent 52c0157a1a
commit 6738cc2a62
2 changed files with 63 additions and 11 deletions
+61
View File
@@ -0,0 +1,61 @@
<script lang="ts">
export let content: string;
export let searchQuery: string;
interface TextPart {
text: string;
isMatch: boolean;
}
function getHighlightedParts(text: string, query: string): TextPart[] {
if (!query) {
return [{ text, isMatch: false }];
}
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`(${escapedQuery})`, "gi");
const parts: TextPart[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
// Add non-matching text before the match
if (match.index > lastIndex) {
parts.push({
text: text.slice(lastIndex, match.index),
isMatch: false,
});
}
// Add the matching text
parts.push({
text: match[1],
isMatch: true,
});
lastIndex = regex.lastIndex;
}
// Add any remaining text after the last match
if (lastIndex < text.length) {
parts.push({
text: text.slice(lastIndex),
isMatch: false,
});
}
return parts;
}
$: parts = getHighlightedParts(content, searchQuery);
</script>
<span class="whitespace-pre-wrap">
{#each parts as part, index (index)}
{#if part.isMatch}
<mark class="search-highlight">{part.text}</mark>
{:else}
{part.text}
{/if}
{/each}
</span>
+2 -11
View File
@@ -3,6 +3,7 @@
import { afterUpdate } from "svelte";
import ConversationTabs from "./ConversationTabs.svelte";
import Markdown from "./Markdown.svelte";
import HighlightedText from "./HighlightedText.svelte";
import { searchState, searchQuery } from "$lib/stores/search";
let terminalElement: HTMLDivElement;
@@ -71,13 +72,6 @@
});
}
function highlightText(text: string, query: string): string {
if (!query) return text;
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`(${escapedQuery})`, "gi");
return text.replace(regex, '<mark class="search-highlight">$1</mark>');
}
$: {
if (currentSearchQuery && lines.length > 0) {
const escapedQuery = currentSearchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -134,10 +128,7 @@
{#if line.type === "assistant"}
<Markdown content={line.content} searchQuery={currentSearchQuery} />
{:else}
<!-- eslint-disable-next-line svelte/no-at-html-tags -- Search highlighting on internal terminal content -->
<span class="whitespace-pre-wrap"
>{@html highlightText(line.content, currentSearchQuery)}</span
>
<HighlightedText content={line.content} searchQuery={currentSearchQuery} />
{/if}
</div>
{/each}