diff --git a/src/utils/sanitize.test.ts b/src/utils/sanitize.test.ts new file mode 100644 index 00000000..ea7bede1 --- /dev/null +++ b/src/utils/sanitize.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from 'vitest'; +import { sanitizeHtml } from './sanitize'; + +describe('sanitizeHtml iframe handling', () => { + test('strips non-YouTube iframe', () => { + const html = ''; + const result = sanitizeHtml(html); + expect(result).not.toContain('iframe'); + }); + + test('allows YouTube nocookie iframe with allowfullscreen', () => { + const html = + ''; + const result = sanitizeHtml(html); + // src should remain + expect(result).toContain('src="https://www.youtube-nocookie.com/embed/abc"'); + // allowfullscreen should be present (may be normalized) + expect(result).toMatch(/allowfullscreen(?:="")?/); + }); +}); diff --git a/src/utils/sanitize.ts b/src/utils/sanitize.ts index d2c95da2..d3d44f15 100644 --- a/src/utils/sanitize.ts +++ b/src/utils/sanitize.ts @@ -28,13 +28,42 @@ if (typeof window !== 'undefined' && !_hookRegistered) { // new URL() throws for relative URLs — allow them (they stay on the same origin) } }); + + DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName === 'IFRAME') { + const src = node.getAttribute('src'); + if (src === null) return; + let allowed = false; + try { + const parsed = new URL(src); + // Only allow YouTube nocookie domain + if ( + SAFE_URL_SCHEMES.includes(parsed.protocol) && + parsed.hostname.endsWith('youtube-nocookie.com') + ) { + allowed = true; + } + } catch { + // Invalid URL – not allowed + } + if (!allowed) { + node.removeAttribute('src'); + node.removeAttribute('allowfullscreen'); + return; + } + // Preserve allowfullscreen if present on allowed iframe + if (node.hasAttribute('allowfullscreen')) { + node.setAttribute('allowfullscreen', ''); + } + } + }); } export const sanitizeHtml = (html: string): string => { if (typeof window === 'undefined') return html; return DOMPurify.sanitize(html, { ADD_TAGS: ['iframe'], - ADD_ATTR: ['allowfullscreen', 'frameborder', 'data-youtube-video'], + ADD_ATTR: ['frameborder', 'src', 'allowfullscreen'], }); };