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'],
});
};