fix(#M5-QA): address security findings from code review

Fixes 2 important-level security issues identified in M5 QA:

1. XSS Protection (SearchResults.tsx):
   - Add DOMPurify sanitization for search result snippets
   - Configure to allow only <mark> tags for highlighting
   - Provides defense-in-depth against potential XSS

2. Error State (SearchPage):
   - Add user-facing error message when search fails
   - Display friendly error notification instead of silent failure
   - Improves UX by informing users of temporary issues

Testing:
- All 32 search component tests passing
- TypeScript typecheck passing
- DOMPurify properly sanitizes HTML while preserving highlighting

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-02 16:50:38 -06:00
parent 0e64dc8525
commit 6e63508f97
4 changed files with 60 additions and 5 deletions

View File

@@ -23,6 +23,7 @@ export default function SearchPage(): React.JSX.Element {
const [results, setResults] = useState<SearchResult[]>([]);
const [totalResults, setTotalResults] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedStatus, setSelectedStatus] = useState<string | undefined>();
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
@@ -51,6 +52,7 @@ export default function SearchPage(): React.JSX.Element {
}
setIsLoading(true);
setError(null);
try {
// Build query params
const params = new URLSearchParams({ q: query });
@@ -67,6 +69,7 @@ export default function SearchPage(): React.JSX.Element {
setTotalResults(response.pagination.total);
} catch (error) {
console.error("Search failed:", error);
setError("Search temporarily unavailable. Please try again in a moment.");
setResults([]);
setTotalResults(0);
} finally {
@@ -99,8 +102,21 @@ export default function SearchPage(): React.JSX.Element {
</div>
</div>
{/* Error state */}
{error && (
<div className="container mx-auto max-w-6xl mt-6">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex items-start gap-3">
<span className="text-2xl"></span>
<div className="flex-1">
<h3 className="font-semibold text-yellow-900 mb-1">Search Unavailable</h3>
<p className="text-yellow-800 text-sm">{error}</p>
</div>
</div>
</div>
)}
{/* Results area */}
{query && (
{query && !error && (
<div className="container mx-auto max-w-7xl">
<SearchResults
results={results}

View File

@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import DOMPurify from "dompurify";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { SearchFilters } from "./SearchFilters";
@@ -50,7 +51,12 @@ function SearchResultItem({ result }: { result: SearchResult }): React.JSX.Eleme
{result.headline && (
<div
className="text-sm text-gray-600 line-clamp-2"
dangerouslySetInnerHTML={{ __html: result.headline }}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(result.headline, {
ALLOWED_TAGS: ["mark"],
ALLOWED_ATTR: [],
}),
}}
/>
)}