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:
@@ -21,9 +21,11 @@
|
||||
"@mosaic/shared": "workspace:*",
|
||||
"@mosaic/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@xyflow/react": "^12.5.3",
|
||||
"better-auth": "^1.4.17",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"elkjs": "^0.9.3",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.4.1",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: [],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user