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.
This commit is contained in:
AmirReza Jamali
2025-11-15 13:28:30 +03:30
parent 1d1027fab4
commit 62f3b49ed4
2 changed files with 85 additions and 4 deletions
@@ -1,6 +1,7 @@
"use client"; "use client";
import { TNotificationDetailsResponse } from "@/lib/api/types/NotificationHistory"; import { TNotificationDetailsResponse } from "@/lib/api/types/NotificationHistory";
import { capitalize, unixToDate } from "@/utils/shared"; import { capitalize, unixToDate } from "@/utils/shared";
import { sanitizeHtmlWithStyles } from "@/lib/utils";
import { FC } from "react"; import { FC } from "react";
import IsVisible from "./IsVisible"; import IsVisible from "./IsVisible";
import Status from "./Status"; import Status from "./Status";
@@ -43,7 +44,12 @@ export const NotificationDetailsCard: FC<IProps> = ({ data }) => {
<IsVisible condition={!!data.message}> <IsVisible condition={!!data.message}>
<hr className="my-5" /> <hr className="my-5" />
<DetailItem title="Message"> <DetailItem title="Message">
<p className="text-base leading-relaxed">{data.message}</p> <div
className="text-base leading-relaxed"
dangerouslySetInnerHTML={{
__html: sanitizeHtmlWithStyles(data.message || '')
}}
/>
</DetailItem> </DetailItem>
</IsVisible> </IsVisible>
</div> </div>
+78 -3
View File
@@ -1,6 +1,81 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { 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(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, "")
.replace(/<object[^>]*>[\s\S]*?<\/object>/gi, "")
.replace(/<embed[^>]*>/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(/&nbsp;/g, " ")
.trim();
} }