diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..39c6dcd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,103 @@ +# Security - XSS Protection + +This document outlines the XSS (Cross-Site Scripting) protection measures implemented in this application. + +## Overview + +All external data from the CMS API is sanitized using DOMPurify before being rendered to prevent XSS attacks. + +## Sanitization Functions + +Located in `lib/sanitize.ts`: + +### `sanitizeText()` + +Strips all HTML tags from text. Use this for plain text content that should never contain HTML. + +```typescript +import { sanitizeText } from "@/lib/sanitize"; + +// Example usage +

{sanitizeText(cmsData?.title)}

; +``` + +### `sanitizeHtml()` + +Allows specific HTML tags while removing dangerous content. Use this when you need to render formatted text. + +```typescript +import { sanitizeHtml } from "@/lib/sanitize"; + +// Example usage with default allowed tags (b, i, em, strong, a, br, p) +
+ +// Example with custom allowed tags +
+``` + +### `sanitizeUrl()` + +Sanitizes URLs to prevent javascript:, data:, and vbscript: URI attacks. + +```typescript +import { sanitizeUrl } from "@/lib/sanitize"; + +// Example usage +Click here + +``` + +## Protected Areas + +The following components have been secured: + +1. **Marketing Pages** (`app/marketing/[id]/page.tsx`) + + - Hero section (title, description, button text, links) + - Features section (titles, descriptions, icons) + - Challenges section (titles, descriptions, icons) + - Advantages section (titles, descriptions, icons) + - Footer (email, phone, location, tagline, copyright) + +2. **Inquiries Form** (`app/_inquires/form.tsx`, `app/_inquires/page.tsx`) + - Form field labels and placeholders + - Submit button text + - Section titles and descriptions + +## Best Practices + +1. **Always sanitize external data** - Any data from APIs, databases, or user input should be sanitized +2. **Use the right function** - Use `sanitizeText()` for plain text, `sanitizeUrl()` for URLs, and `sanitizeHtml()` for formatted content +3. **Validate on the server** - While client-side sanitization helps, always validate and sanitize on the server as well +4. **Keep DOMPurify updated** - Regularly update the DOMPurify package to get the latest security fixes + +## Server-Side Rendering + +The sanitization functions work in both client and server environments: + +- **Server-side**: Uses HTML entity escaping to prevent XSS attacks +- **Client-side**: Uses DOMPurify for advanced HTML sanitization + +This hybrid approach ensures: + +1. Fast server-side rendering without heavy dependencies +2. Robust client-side protection with DOMPurify +3. No build issues with Node.js-specific modules + +## Dependencies + +- `dompurify` (^3.3.0) - HTML sanitization library (client-side only) + +## Testing XSS Protection + +To verify XSS protection is working, try injecting malicious content through the CMS: + +```javascript +// These should be sanitized and rendered harmless: + + +javascript:alert('XSS') + +``` + +All of these should be stripped or neutralized by the sanitization functions. diff --git a/USAGE_EXAMPLES.md b/USAGE_EXAMPLES.md new file mode 100644 index 0000000..fdadf6a --- /dev/null +++ b/USAGE_EXAMPLES.md @@ -0,0 +1,222 @@ +# XSS Protection Usage Examples + +## Basic Usage + +### Sanitizing Text Content + +```typescript +import { sanitizeText } from "@/lib/sanitize"; + +// In a component +function MyComponent({ cmsData }) { + return ( +
+

{sanitizeText(cmsData.title)}

+

{sanitizeText(cmsData.description)}

+
+ ); +} +``` + +### Sanitizing URLs + +```typescript +import { sanitizeUrl } from "@/lib/sanitize"; +import Link from "next/link"; +import Image from "next/image"; + +function MyComponent({ cmsData }) { + return ( +
+ {/* For links */} + Click here + + {/* For images */} + {sanitizeText(cmsData.imageAlt)} +
+ ); +} +``` + +### Sanitizing HTML Content + +```typescript +import { sanitizeHtml } from "@/lib/sanitize"; + +function MyComponent({ cmsData }) { + return ( +
+ {/* With default allowed tags (b, i, em, strong, a, br, p) */} +
+ + {/* With custom allowed tags */} +
+
+ ); +} +``` + +## Real-World Examples + +### Example 1: Dynamic Marketing Page + +```typescript +import { sanitizeText, sanitizeUrl } from "@/lib/sanitize"; + +export default function MarketingPage({ cmsData }) { + return ( +
+
+

{sanitizeText(cmsData.hero.title)}

+

{sanitizeText(cmsData.hero.description)}

+ + {sanitizeText(cmsData.hero.buttonText)} + +
+ +
+ {cmsData.features.map((feature) => ( +
+ {sanitizeText(feature.title)} +

{sanitizeText(feature.title)}

+

{sanitizeText(feature.description)}

+
+ ))} +
+
+ ); +} +``` + +### Example 2: Dynamic Form Fields + +```typescript +import { sanitizeText } from "@/lib/sanitize"; +import { useForm } from "react-hook-form"; + +export default function DynamicForm({ contactData }) { + const { register, handleSubmit } = useForm(); + + return ( +
+ {contactData.fields.map((field) => ( +
+ + +
+ ))} + +
+ ); +} +``` + +### Example 3: User-Generated Content + +```typescript +import { sanitizeHtml } from "@/lib/sanitize"; + +export default function CommentSection({ comments }) { + return ( +
+ {comments.map((comment) => ( +
+

{sanitizeText(comment.author)}

+ {/* Allow only basic formatting tags */} +
+
+ ))} +
+ ); +} +``` + +## Common XSS Attack Vectors (All Blocked) + +```typescript +// These malicious inputs will be sanitized: + +// 1. Script injection +const xss1 = ""; +sanitizeText(xss1); // Safe: <script>alert('XSS')</script> + +// 2. Event handler injection +const xss2 = ""; +sanitizeText(xss2); // Safe: <img src=x onerror=alert('XSS')> + +// 3. JavaScript URL +const xss3 = "javascript:alert('XSS')"; +sanitizeUrl(xss3); // Returns: "" + +// 4. Data URL with script +const xss4 = "data:text/html,"; +sanitizeUrl(xss4); // Returns: "" + +// 5. Iframe injection +const xss5 = ""; +sanitizeText(xss5); // Safe: <iframe src='javascript:alert(1)'></iframe> +``` + +## Best Practices + +1. **Always sanitize external data** before rendering +2. **Use the appropriate function** for the content type +3. **Provide fallback values** for URLs and optional content +4. **Never trust user input** or external API data +5. **Keep dependencies updated** to get latest security patches + +## When NOT to Sanitize + +- Static content defined in your codebase +- Environment variables (but validate them) +- Data from trusted internal APIs (but still validate) +- Constants and configuration values + +## Testing Your Sanitization + +```typescript +// Test with malicious input +const testData = { + title: "Hello", + link: "javascript:alert('XSS')", + description: "", +}; + +// All should be safe +console.log(sanitizeText(testData.title)); // No script execution +console.log(sanitizeUrl(testData.link)); // Empty string +console.log(sanitizeText(testData.description)); // No image/script +``` diff --git a/XSS_PROTECTION_SUMMARY.md b/XSS_PROTECTION_SUMMARY.md new file mode 100644 index 0000000..205ce85 --- /dev/null +++ b/XSS_PROTECTION_SUMMARY.md @@ -0,0 +1,134 @@ +# XSS Protection Implementation Summary + +## What Was Done + +Successfully implemented comprehensive XSS (Cross-Site Scripting) protection across the application using DOMPurify. + +## Files Created + +1. **`lib/sanitize.ts`** - Core sanitization utilities + + - `sanitizeText()` - Strips all HTML tags + - `sanitizeHtml()` - Allows specific HTML tags + - `sanitizeUrl()` - Validates and sanitizes URLs + +2. **`SECURITY.md`** - Security documentation +3. **`USAGE_EXAMPLES.md`** - Practical usage examples +4. **`lib/__tests__/sanitize.test.ts`** - Unit tests + +## Files Modified + +1. **`app/marketing/[id]/page.tsx`** + + - Sanitized all CMS data: titles, descriptions, URLs, icons + - Protected hero section, features, challenges, advantages, footer + +2. **`app/_inquires/form.tsx`** + + - Sanitized form field labels and placeholders + - Sanitized submit button text + +3. **`app/_inquires/page.tsx`** + - Sanitized section titles and descriptions + +## Protected Data Points + +### Marketing Page (`/marketing/[id]`) + +- ✅ Hero title and description +- ✅ Hero button text and link +- ✅ Feature cards (titles, descriptions, icons) +- ✅ Challenge cards (titles, descriptions, icons) +- ✅ Advantage cards (titles, descriptions, icons) +- ✅ Footer (email, phone, location, tagline, copyright) + +### Inquiries Form + +- ✅ Form field labels +- ✅ Form field placeholders +- ✅ Submit button text +- ✅ Section titles and descriptions + +## How It Works + +### Server-Side (SSR) + +- Uses HTML entity escaping +- Fast and lightweight +- No external dependencies needed + +### Client-Side + +- Uses DOMPurify library +- Advanced HTML sanitization +- Configurable allowed tags + +## Security Features + +1. **Script Injection Protection** + + - Blocks ` + +javascript:alert('XSS') +``` + +All malicious content will be escaped or removed before rendering. diff --git a/app/_inquires/form.tsx b/app/_inquires/form.tsx index 2da6d63..5d79527 100644 --- a/app/_inquires/form.tsx +++ b/app/_inquires/form.tsx @@ -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 (
{

- {contactData?.title || "Unlock Your Growth Potential"} + {sanitizeText(contactData?.title) || "Unlock Your Growth Potential"}

- {contactData?.description || + {sanitizeText(contactData?.description) || "Register for early access and a free consultation. Let's start winning together."}

diff --git a/app/marketing/[id]/page.tsx b/app/marketing/[id]/page.tsx index 35935e9..3f97511 100644 --- a/app/marketing/[id]/page.tsx +++ b/app/marketing/[id]/page.tsx @@ -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) {

- {cmsData?.data.hero.title} + {sanitizeText(cmsData?.data.hero.title)}

- {cmsData?.data.hero.description} + {sanitizeText(cmsData?.data.hero.description)}

- {cmsData?.data.hero.button_text} + href={sanitizeUrl(cmsData?.data.hero.button_link) || "#"}> + {sanitizeText(cmsData?.data.hero.button_text)}

- {cmsData?.data.features.title} + {sanitizeText(cmsData?.data.features.title)}

- {cmsData?.data.features.description} + {sanitizeText(cmsData?.data.features.description)}
@@ -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"> -

{frame.title}

-

{frame.description}

+

+ {sanitizeText(frame.title)} +

+

{sanitizeText(frame.description)}

))}

- {cmsData?.data.challenges.title} + {sanitizeText(cmsData?.data.challenges.title)}

- {cmsData?.data.challenges.description} + {sanitizeText(cmsData?.data.challenges.description)}

    @@ -150,15 +153,15 @@ export default async function Home({ params }: IProps) { key={card.title}>
    -

    {card.title}

    -

    {card.description}

    +

    {sanitizeText(card.title)}

    +

    {sanitizeText(card.description)}

    ))}
@@ -166,7 +169,7 @@ export default async function Home({ params }: IProps) {

- {cmsData?.data.advantages.title} + {sanitizeText(cmsData?.data.advantages.title)}

@@ -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}>
-

{card.title}

-

{card.description}

+

+ {sanitizeText(card.title)} +

+

{sanitizeText(card.description)}

))} @@ -210,10 +215,10 @@ export default async function Home({ params }: IProps) { />

- {cmsData?.data.footer.tagline} + {sanitizeText(cmsData?.data.footer.tagline)}

- {cmsData?.data.footer.copyright} + {sanitizeText(cmsData?.data.footer.copyright)}
@@ -227,8 +232,8 @@ export default async function Home({ params }: IProps) { alt="" aria-hidden="true" /> - - {cmsData?.data.footer.phone} + + {sanitizeText(cmsData?.data.footer.phone)}
  • @@ -239,8 +244,9 @@ export default async function Home({ params }: IProps) { alt="" aria-hidden="true" /> - - {cmsData?.data.footer.email} + + {sanitizeText(cmsData?.data.footer.email)}
  • @@ -251,7 +257,7 @@ export default async function Home({ params }: IProps) { alt="" aria-hidden="true" /> -

    {cmsData?.data.footer.location}

    +

    {sanitizeText(cmsData?.data.footer.location)}

  • diff --git a/lib/__tests__/sanitize.test.ts b/lib/__tests__/sanitize.test.ts new file mode 100644 index 0000000..9c2d4fe --- /dev/null +++ b/lib/__tests__/sanitize.test.ts @@ -0,0 +1,61 @@ +import { sanitizeHtml, sanitizeText, sanitizeUrl } from "../sanitize"; + +describe("Sanitization Functions", () => { + describe("sanitizeText", () => { + it("should remove HTML tags", () => { + const dirty = "Hello"; + const clean = sanitizeText(dirty); + expect(clean).not.toContain(""); + }); + + it("should handle null and undefined", () => { + expect(sanitizeText(null)).toBe(""); + expect(sanitizeText(undefined)).toBe(""); + }); + + it("should escape dangerous characters", () => { + const dirty = ""; + const clean = sanitizeText(dirty); + expect(clean).not.toContain(" { + it("should block javascript: URLs", () => { + const dirty = "javascript:alert('XSS')"; + const clean = sanitizeUrl(dirty); + expect(clean).toBe(""); + }); + + it("should block data: URLs", () => { + const dirty = "data:text/html,"; + const clean = sanitizeUrl(dirty); + expect(clean).toBe(""); + }); + + it("should allow safe URLs", () => { + const safe = "https://example.com"; + const clean = sanitizeUrl(safe); + expect(clean).toBeTruthy(); + }); + + it("should handle null and undefined", () => { + expect(sanitizeUrl(null)).toBe(""); + expect(sanitizeUrl(undefined)).toBe(""); + }); + }); + + describe("sanitizeHtml", () => { + it("should remove script tags", () => { + const dirty = "

    Hello

    "; + const clean = sanitizeHtml(dirty); + expect(clean).not.toContain("