diff --git a/src/app/credentials/[credentialId]/page.tsx b/src/app/credentials/[credentialId]/page.tsx
index f86c676..04754c5 100644
--- a/src/app/credentials/[credentialId]/page.tsx
+++ b/src/app/credentials/[credentialId]/page.tsx
@@ -67,8 +67,17 @@ export default function CredentialDetailPage({
const handleDownload = useCallback(() => {
if (!credential) return;
+ // #489 — escape all user-supplied values before interpolating into HTML
+ // to prevent XSS via malicious course titles or IDs.
+ const esc = (s: string | number | null | undefined): string =>
+ String(s ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
const html = `
-
Certificate of Completion
-
${credential.courseTitle}
+
${esc(credential.courseTitle)}
-
Issued: ${formatDate(credential.issuedAt)}
-
Credential ID: ${credential.id}
-
Token: ${truncateAddress(credential.tokenId, 8)}
- ${credential.metadata.score ? `
Score: ${credential.metadata.score}%
` : ""}
+
Issued: ${esc(formatDate(credential.issuedAt))}
+
Credential ID: ${esc(credential.id)}
+
Token: ${esc(truncateAddress(credential.tokenId, 8))}
+ ${credential.metadata.score ? `
Score: ${esc(credential.metadata.score)}%
` : ""}
Verified on Stellar blockchain
@@ -91,7 +100,7 @@ export default function CredentialDetailPage({
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
- a.download = `certificate-${credential.courseTitle.toLowerCase().replace(/\s+/g, "-")}.html`;
+ a.download = `certificate-${credential.courseTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.html`;
a.click();
URL.revokeObjectURL(url);
}, [credential]);
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 5e70728..1654d59 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,5 +1,6 @@
"use client";
+import { Component, type ReactNode } from "react";
import { useRequireAuth } from "@/lib/hooks/use-require-auth";
import { useCourses, useRecommendedCourses } from "@/lib/hooks/use-courses";
import { useRewards } from "@/lib/hooks/use-rewards";
@@ -20,17 +21,58 @@ import {
Award,
TrendingUp,
ArrowRight,
+ AlertTriangle,
} from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+// #488 — lightweight per-section error boundary so one failing section
+// (rewards, credentials, etc.) never takes down the whole dashboard.
+class SectionErrorBoundary extends Component<
+ { label: string; children: ReactNode },
+ { hasError: boolean }
+> {
+ state = { hasError: false };
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+