From a17c118d1740e83c5829605af2c2168b7a49911e Mon Sep 17 00:00:00 2001 From: Ranjeet2063 Date: Thu, 1 Oct 2026 13:10:04 +0545 Subject: [PATCH] fix(security): sanitize unescaped HTML interpolation in certificate download --- src/app/credentials/[credentialId]/page.tsx | 34 +++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) 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 + ? `

Score: ${htmlEscape(credential.metadata.score)}%

` + : ""; + const html = ` -Certificate - ${credential.courseTitle} +Certificate - ${escapedTitle}

Certificate of Completion

-

${credential.courseTitle}

+

${escapedTitle}

-

Issued: ${formatDate(credential.issuedAt)}

-

Credential ID: ${credential.id}

-

Token: ${truncateAddress(credential.tokenId, 8)}

- ${credential.metadata.score ? `

Score: ${credential.metadata.score}%

` : ""} +

Issued: ${escapedIssued}

+

Credential ID: ${escapedId}

+

Token: ${escapedTokenId}

+ ${scoreHtml}

Verified on Stellar blockchain