generated from nhcarrigan/template
fix: resolve XSS vulnerability in Terminal component search highlighting
- 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:
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user