diff --git a/src/app/credentials/[credentialId]/page.tsx b/src/app/credentials/[credentialId]/page.tsx index f86c676..c9f9ccf 100644 --- a/src/app/credentials/[credentialId]/page.tsx +++ b/src/app/credentials/[credentialId]/page.tsx @@ -27,6 +27,20 @@ import { import { useState, useCallback } from "react"; import { ShareButton } from "@/components/shared/share-button"; +/** + * Safely escapes HTML special characters to prevent cross-site scripting (XSS) + * when interpolating dynamic data into raw certificate HTML templates. + */ +export function htmlEscape(value: unknown): string { + if (value === null || value === undefined) return ""; + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + export default function CredentialDetailPage({ params, }: { @@ -67,8 +81,16 @@ export default function CredentialDetailPage({ const handleDownload = useCallback(() => { if (!credential) return; + const escapedTitle = htmlEscape(credential.courseTitle); + const escapedIssued = htmlEscape(formatDate(credential.issuedAt)); + const escapedId = htmlEscape(credential.id); + const escapedTokenId = htmlEscape(truncateAddress(credential.tokenId, 8)); + const scoreHtml = credential.metadata?.score != null + ? `
` + : ""; + const html = ` -