From b3a5cc4f8871e6a73f9dbfa15520af400a6a35e3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 12 Jan 2026 22:09:50 +0530 Subject: [PATCH] feat: sanitize html before assiging it to tempDiv --- .../dashboard/helper/emailQuoteExtractor.js | 6 ++- .../helper/specs/emailQuoteExtractor.spec.js | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/helper/emailQuoteExtractor.js b/app/javascript/dashboard/helper/emailQuoteExtractor.js index 775dc4db4..f29d48cca 100644 --- a/app/javascript/dashboard/helper/emailQuoteExtractor.js +++ b/app/javascript/dashboard/helper/emailQuoteExtractor.js @@ -1,3 +1,5 @@ +import DOMPurify from 'dompurify'; + // Quote detection strategies const QUOTE_INDICATORS = [ '.gmail_quote_container', @@ -29,7 +31,7 @@ export class EmailQuoteExtractor { static extractQuotes(htmlContent) { // Create a temporary DOM element to parse HTML const tempDiv = document.createElement('div'); - tempDiv.innerHTML = htmlContent; + tempDiv.innerHTML = DOMPurify.sanitize(htmlContent); // Remove elements matching class selectors QUOTE_INDICATORS.forEach(selector => { @@ -56,7 +58,7 @@ export class EmailQuoteExtractor { */ static hasQuotes(htmlContent) { const tempDiv = document.createElement('div'); - tempDiv.innerHTML = htmlContent; + tempDiv.innerHTML = DOMPurify.sanitize(htmlContent); // Check for class-based quotes // eslint-disable-next-line no-restricted-syntax diff --git a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js index 7bd2aaa51..20b50581b 100644 --- a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js +++ b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js @@ -96,4 +96,58 @@ describe('EmailQuoteExtractor', () => { it('detects quotes for trailing blockquotes even when signatures follow text', () => { expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true); }); + + describe('HTML sanitization', () => { + it('removes onerror handlers from img tags in extractQuotes', () => { + const maliciousHtml = '

Hello

'; + const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); + + expect(cleanedHtml).not.toContain('onerror'); + expect(cleanedHtml).toContain('

Hello

'); + }); + + it('removes onerror handlers from img tags in hasQuotes', () => { + const maliciousHtml = '

Hello

'; + // Should not throw and should safely check for quotes + const result = EmailQuoteExtractor.hasQuotes(maliciousHtml); + expect(result).toBe(false); + }); + + it('removes script tags in extractQuotes', () => { + const maliciousHtml = + '

Content

More

'; + const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); + + expect(cleanedHtml).not.toContain('Content

'); + expect(cleanedHtml).toContain('

More

'); + }); + + it('removes onclick handlers in extractQuotes', () => { + const maliciousHtml = '

Click me

'; + const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); + + expect(cleanedHtml).not.toContain('onclick'); + expect(cleanedHtml).toContain('Click me'); + }); + + it('removes javascript: URLs in extractQuotes', () => { + const maliciousHtml = 'Link'; + const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); + + // eslint-disable-next-line no-script-url + expect(cleanedHtml).not.toContain('javascript:'); + expect(cleanedHtml).toContain('Link'); + }); + + it('removes encoded payloads with event handlers in extractQuotes', () => { + const maliciousHtml = + ''; + const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); + + expect(cleanedHtml).not.toContain('onerror'); + expect(cleanedHtml).not.toContain('eval'); + }); + }); });