From 62f3b49ed4445f866c2d0d515aea82d6d992be21 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sat, 15 Nov 2025 13:28:30 +0330 Subject: [PATCH] feat: add HTML sanitization for notification messages Implement sanitizeHtmlWithStyles utility to safely render HTML content in notification messages while removing dangerous tags and attributes. Update NotificationDetailsCard to use dangerouslySetInnerHTML with sanitization for message display. --- .../ui/notification-details-card.tsx | 8 +- src/lib/utils.ts | 81 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/components/ui/notification-details-card.tsx b/src/components/ui/notification-details-card.tsx index 54332f7..fb05bbf 100644 --- a/src/components/ui/notification-details-card.tsx +++ b/src/components/ui/notification-details-card.tsx @@ -1,6 +1,7 @@ "use client"; import { TNotificationDetailsResponse } from "@/lib/api/types/NotificationHistory"; import { capitalize, unixToDate } from "@/utils/shared"; +import { sanitizeHtmlWithStyles } from "@/lib/utils"; import { FC } from "react"; import IsVisible from "./IsVisible"; import Status from "./Status"; @@ -43,7 +44,12 @@ export const NotificationDetailsCard: FC = ({ data }) => {
-

{data.message}

+
diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..1a0bb60 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,81 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); +} + +export function sanitizeHtmlWithStyles(htmlString: string): string { + if (!htmlString) return ""; + + if (typeof window !== "undefined") { + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlString; + + const dangerousTags = [ + "script", + "iframe", + "object", + "embed", + "form", + "input", + "button", + ]; + const dangerousAttributes = [ + "onclick", + "onload", + "onerror", + "onmouseover", + "onfocus", + "onblur", + ]; + + dangerousTags.forEach((tag) => { + const elements = tempDiv.getElementsByTagName(tag); + for (let i = elements.length - 1; i >= 0; i--) { + elements[i].remove(); + } + }); + + const allElements = tempDiv.getElementsByTagName("*"); + for (let i = 0; i < allElements.length; i++) { + const element = allElements[i]; + dangerousAttributes.forEach((attr) => { + if (element.hasAttribute(attr)) { + element.removeAttribute(attr); + } + }); + + Array.from(element.attributes).forEach((attr) => { + if (attr.name.toLowerCase().startsWith("on")) { + element.removeAttribute(attr.name); + } + }); + } + + return tempDiv.innerHTML; + } + + return htmlString + .replace(/]*>[\s\S]*?<\/script>/gi, "") + .replace(/]*>[\s\S]*?<\/iframe>/gi, "") + .replace(/]*>[\s\S]*?<\/object>/gi, "") + .replace(/]*>/gi, "") + .replace(/on\w+="[^"]*"/gi, "") + .replace(/on\w+='[^']*'/gi, ""); +} + +export function stripHtmlTags(htmlString: string): string { + if (!htmlString) return ""; + + if (typeof window !== "undefined") { + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = htmlString; + return tempDiv.textContent || tempDiv.innerText || ""; + } + + return htmlString + .replace(/<[^>]*>/g, "") + .replace(/ /g, " ") + .trim(); }