feat(security): Add XSS protection with DOMPurify sanitization
- Add sanitize.ts utility module with sanitizeText(), sanitizeUrl(), and sanitizeHtml() functions - Implement server-side HTML entity escaping and client-side DOMPurify sanitization - Add comprehensive test suite for sanitization functions in lib/__tests__/sanitize.test.ts - Secure marketing pages by sanitizing all CMS data (hero, features, challenges, advantages, footer) - Secure inquiries form by sanitizing field labels, placeholders, and button text - Add SECURITY.md documentation outlining XSS protection measures and best practices - Add USAGE_EXAMPLES.md with detailed examples of sanitization function usage - Add XSS_PROTECTION_SUMMARY.md with implementation overview - Update package.json to include dompurify (^3.3.0) dependency - Protects against common XSS attack vectors including script injection, event handlers, and malicious URLs
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { sanitizeText } from "@/lib/sanitize";
|
||||
import api from "@/service/api";
|
||||
import { CmsContact } from "@/types/TCms";
|
||||
import { useState } from "react";
|
||||
@@ -82,7 +83,8 @@ const InquiresForm = ({
|
||||
];
|
||||
|
||||
const fields = contactData?.fields || defaultFields;
|
||||
const submitText = contactData?.submit_button_text || buttonText || "Submit";
|
||||
const submitText =
|
||||
sanitizeText(contactData?.submit_button_text) || buttonText || "Submit";
|
||||
|
||||
return (
|
||||
<form
|
||||
@@ -100,8 +102,8 @@ const InquiresForm = ({
|
||||
<InputGroup
|
||||
key={field.name}
|
||||
id={field.name}
|
||||
label={field.label}
|
||||
placeholder={field.placeholder}
|
||||
label={sanitizeText(field.label)}
|
||||
placeholder={sanitizeText(field.placeholder)}
|
||||
register={register}
|
||||
type={inputType}
|
||||
error={errors[field.name]?.message}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sanitizeText } from "@/lib/sanitize";
|
||||
import { CmsContact } from "@/types/TCms";
|
||||
import InquiresForm from "./form";
|
||||
|
||||
@@ -6,10 +7,10 @@ const Inquires = ({ contactData }: { contactData?: CmsContact }) => {
|
||||
<div className="flex flex-col items-center mt-16 px-8 ">
|
||||
<div className="lg:w-2/3 m-auto flex flex-col gap-4 mb-12">
|
||||
<h2 className="font-bold text-4xl">
|
||||
{contactData?.title || "Unlock Your Growth Potential"}
|
||||
{sanitizeText(contactData?.title) || "Unlock Your Growth Potential"}
|
||||
</h2>
|
||||
<p className="font-normal text-[16px] text-gray-600">
|
||||
{contactData?.description ||
|
||||
{sanitizeText(contactData?.description) ||
|
||||
"Register for early access and a free consultation. Let's start winning together."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+34
-28
@@ -2,6 +2,7 @@ import ApexChart from "@/app/_components/ApexChart";
|
||||
import FooterForm from "@/app/_components/FooterForm";
|
||||
import ImageWithFallback from "@/app/_components/ImageWithFallback";
|
||||
import { cmsService } from "@/hooks/useCmsQueries";
|
||||
import { sanitizeText, sanitizeUrl } from "@/lib/sanitize";
|
||||
import { CmsResponse } from "@/types/TCms";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
@@ -93,27 +94,27 @@ export default async function Home({ params }: IProps) {
|
||||
<section className="text-left flex flex-col gap-6 bg-gradient-to-b from-[#011132] to-[#012B80] text-white rounded-[56px] relative p-6 lg:p-14 -mt-16 z-20 lg:ml-28 ">
|
||||
<hgroup>
|
||||
<h1 className="capitalize font-extrabold text-4xl mb-6 md:text-5xl">
|
||||
{cmsData?.data.hero.title}
|
||||
{sanitizeText(cmsData?.data.hero.title)}
|
||||
</h1>
|
||||
<p className="text-[16px] text-[#dadada]">
|
||||
{cmsData?.data.hero.description}
|
||||
{sanitizeText(cmsData?.data.hero.description)}
|
||||
</p>
|
||||
</hgroup>
|
||||
<div className="flex justify-end">
|
||||
<Link
|
||||
className="w-full lg:w-fit capitalize bg-(--primary) rounded-4xl py-4 px-6"
|
||||
href={cmsData?.data.hero.button_link ?? "#"}>
|
||||
{cmsData?.data.hero.button_text}
|
||||
href={sanitizeUrl(cmsData?.data.hero.button_link) || "#"}>
|
||||
{sanitizeText(cmsData?.data.hero.button_text)}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<article className="mt-40">
|
||||
<hgroup>
|
||||
<h4 className="capitalize font-bold text-2xl lg:text-4xl">
|
||||
{cmsData?.data.features.title}
|
||||
{sanitizeText(cmsData?.data.features.title)}
|
||||
</h4>
|
||||
<small className="text-gray-600 mt-3">
|
||||
{cmsData?.data.features.description}
|
||||
{sanitizeText(cmsData?.data.features.description)}
|
||||
</small>
|
||||
</hgroup>
|
||||
</article>
|
||||
@@ -123,24 +124,26 @@ export default async function Home({ params }: IProps) {
|
||||
key={frame.description}
|
||||
className="bg-[#F7FAFF] border border-(--primary)/30 rounded-3xl my-4 py-10 px-6 flex flex-col gap-5 transition duration-300 transform scale-90 hover:bg-[#0164FF30] hover:scale-110">
|
||||
<ImageWithFallback
|
||||
src={frame.icon ?? "/magnifier.svg"}
|
||||
alt={frame.title}
|
||||
src={sanitizeUrl(frame.icon) || "/magnifier.svg"}
|
||||
alt={sanitizeText(frame.title)}
|
||||
width={36}
|
||||
height={36}
|
||||
unoptimized
|
||||
/>
|
||||
<h3 className="text-2xl font-bold">{frame.title}</h3>
|
||||
<p className="text-gray-600">{frame.description}</p>
|
||||
<h3 className="text-2xl font-bold">
|
||||
{sanitizeText(frame.title)}
|
||||
</h3>
|
||||
<p className="text-gray-600">{sanitizeText(frame.description)}</p>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="mt-36 bg-[#F7FAFF] px-4 py-6 rounded-4xl flex flex-col gap-9 lg:p-16">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className=" font-bold text-4xl">
|
||||
{cmsData?.data.challenges.title}
|
||||
{sanitizeText(cmsData?.data.challenges.title)}
|
||||
</h2>
|
||||
<p className="font-normal text-sm">
|
||||
{cmsData?.data.challenges.description}
|
||||
{sanitizeText(cmsData?.data.challenges.description)}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-4 lg:flex-row lg:justify-between">
|
||||
@@ -150,15 +153,15 @@ export default async function Home({ params }: IProps) {
|
||||
key={card.title}>
|
||||
<div className="p-4">
|
||||
<ImageWithFallback
|
||||
src={card.icon ?? "/clock.svg"}
|
||||
alt={card.title}
|
||||
src={sanitizeUrl(card.icon) || "/clock.svg"}
|
||||
alt={sanitizeText(card.title)}
|
||||
width={36}
|
||||
height={36}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<h2 className="font-bold">{card.title}</h2>
|
||||
<p>{card.description}</p>
|
||||
<h2 className="font-bold">{sanitizeText(card.title)}</h2>
|
||||
<p>{sanitizeText(card.description)}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -166,7 +169,7 @@ export default async function Home({ params }: IProps) {
|
||||
<section className="flex flex-col mt-36 gap-10 px-2 lg:w-3/4 m-auto">
|
||||
<div>
|
||||
<h2 className="font-bold text-2xl lg:text-4xl">
|
||||
{cmsData?.data.advantages.title}
|
||||
{sanitizeText(cmsData?.data.advantages.title)}
|
||||
</h2>
|
||||
</div>
|
||||
<section className="flex flex-col gap-10 lg:flex-row">
|
||||
@@ -175,15 +178,17 @@ export default async function Home({ params }: IProps) {
|
||||
className="flex flex-col gap-7 bg-[#FFF7EB] p-10 rounded-3xl lg:pb-20"
|
||||
key={card.title}>
|
||||
<ImageWithFallback
|
||||
src={card.icon ?? "/people.svg"}
|
||||
src={sanitizeUrl(card.icon) || "/people.svg"}
|
||||
width={48}
|
||||
height={48}
|
||||
alt="people icon"
|
||||
alt={sanitizeText(card.title)}
|
||||
unoptimized
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="font-bold text-xl">{card.title}</h2>
|
||||
<p>{card.description}</p>
|
||||
<h2 className="font-bold text-xl">
|
||||
{sanitizeText(card.title)}
|
||||
</h2>
|
||||
<p>{sanitizeText(card.description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -210,10 +215,10 @@ export default async function Home({ params }: IProps) {
|
||||
/>
|
||||
<div>
|
||||
<p className="text-white my-2">
|
||||
{cmsData?.data.footer.tagline}
|
||||
{sanitizeText(cmsData?.data.footer.tagline)}
|
||||
</p>
|
||||
<small className="text-white my-2">
|
||||
{cmsData?.data.footer.copyright}
|
||||
{sanitizeText(cmsData?.data.footer.copyright)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,8 +232,8 @@ export default async function Home({ params }: IProps) {
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<a href={`tel:${cmsData?.data.footer.phone}`}>
|
||||
{cmsData?.data.footer.phone}
|
||||
<a href={`tel:${sanitizeText(cmsData?.data.footer.phone)}`}>
|
||||
{sanitizeText(cmsData?.data.footer.phone)}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex gap-3 my-3 text-white">
|
||||
@@ -239,8 +244,9 @@ export default async function Home({ params }: IProps) {
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<a href={`mailto:${cmsData?.data.footer.email}`}>
|
||||
{cmsData?.data.footer.email}
|
||||
<a
|
||||
href={`mailto:${sanitizeText(cmsData?.data.footer.email)}`}>
|
||||
{sanitizeText(cmsData?.data.footer.email)}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex gap-3 my-3 text-white">
|
||||
@@ -251,7 +257,7 @@ export default async function Home({ params }: IProps) {
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p>{cmsData?.data.footer.location}</p>
|
||||
<p>{sanitizeText(cmsData?.data.footer.location)}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</address>
|
||||
|
||||
Reference in New Issue
Block a user