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 { afterUpdate } from "svelte";
|
||||||
import ConversationTabs from "./ConversationTabs.svelte";
|
import ConversationTabs from "./ConversationTabs.svelte";
|
||||||
import Markdown from "./Markdown.svelte";
|
import Markdown from "./Markdown.svelte";
|
||||||
|
import HighlightedText from "./HighlightedText.svelte";
|
||||||
import { searchState, searchQuery } from "$lib/stores/search";
|
import { searchState, searchQuery } from "$lib/stores/search";
|
||||||
|
|
||||||
let terminalElement: HTMLDivElement;
|
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) {
|
if (currentSearchQuery && lines.length > 0) {
|
||||||
const escapedQuery = currentSearchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escapedQuery = currentSearchQuery.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
@@ -134,10 +128,7 @@
|
|||||||
{#if line.type === "assistant"}
|
{#if line.type === "assistant"}
|
||||||
<Markdown content={line.content} searchQuery={currentSearchQuery} />
|
<Markdown content={line.content} searchQuery={currentSearchQuery} />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- Search highlighting on internal terminal content -->
|
<HighlightedText content={line.content} searchQuery={currentSearchQuery} />
|
||||||
<span class="whitespace-pre-wrap"
|
|
||||||
>{@html highlightText(line.content, currentSearchQuery)}</span
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user