mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
fix: isolated DOMPurify instance and correct video controls attribute
- Use DOMPurify(window) to avoid global hook pollution - Fix controls default from boolean to empty string (HTML attribute semantics) - Remove redundant non-null assertion on featured_image_url
This commit is contained in:
@@ -14,7 +14,7 @@ export const VideoExtension = Node.create({
|
|||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
src: { default: null },
|
src: { default: null },
|
||||||
controls: { default: true },
|
controls: { default: '' },
|
||||||
class: { default: null },
|
class: { default: null },
|
||||||
preload: { default: 'metadata' },
|
preload: { default: 'metadata' },
|
||||||
};
|
};
|
||||||
@@ -25,6 +25,6 @@ export const VideoExtension = Node.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ['video', mergeAttributes(HTMLAttributes, { controls: '' })];
|
return ['video', mergeAttributes(HTMLAttributes)];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -164,16 +164,17 @@ const SANITIZE_CONFIG = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize HTML with scoped DOMPurify hooks.
|
* Isolated DOMPurify instance for article content sanitization.
|
||||||
*
|
* Using a separate instance avoids polluting the global DOMPurify singleton
|
||||||
* Hooks are registered/removed inside this function to avoid polluting the
|
* that other pages may use with their own hooks/config.
|
||||||
* global DOMPurify instance (other pages use DOMPurify with their own config).
|
|
||||||
*/
|
*/
|
||||||
|
const articlePurify = DOMPurify(window);
|
||||||
|
|
||||||
function sanitizeHtml(html: string): string {
|
function sanitizeHtml(html: string): string {
|
||||||
DOMPurify.removeAllHooks();
|
articlePurify.removeAllHooks();
|
||||||
|
|
||||||
// Hook: strip iframes with disallowed src
|
// Hook: strip iframes with disallowed src
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
articlePurify.addHook('afterSanitizeAttributes', (node) => {
|
||||||
if (node.tagName === 'IFRAME') {
|
if (node.tagName === 'IFRAME') {
|
||||||
const src = node.getAttribute('src') ?? '';
|
const src = node.getAttribute('src') ?? '';
|
||||||
if (!isAllowedIframeSrc(src)) {
|
if (!isAllowedIframeSrc(src)) {
|
||||||
@@ -191,7 +192,7 @@ function sanitizeHtml(html: string): string {
|
|||||||
|
|
||||||
// Hook: validate <video> src — only allow http/https (block javascript:, data:, etc.)
|
// Hook: validate <video> src — only allow http/https (block javascript:, data:, etc.)
|
||||||
// HTTP is permitted because request.base_url behind a reverse proxy returns http://
|
// HTTP is permitted because request.base_url behind a reverse proxy returns http://
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
articlePurify.addHook('afterSanitizeAttributes', (node) => {
|
||||||
if (node.tagName === 'VIDEO') {
|
if (node.tagName === 'VIDEO') {
|
||||||
const src = node.getAttribute('src') ?? '';
|
const src = node.getAttribute('src') ?? '';
|
||||||
try {
|
try {
|
||||||
@@ -210,7 +211,7 @@ function sanitizeHtml(html: string): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Hook: force safe link attributes
|
// Hook: force safe link attributes
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
articlePurify.addHook('afterSanitizeAttributes', (node) => {
|
||||||
if (node.tagName === 'A') {
|
if (node.tagName === 'A') {
|
||||||
node.setAttribute('target', '_blank');
|
node.setAttribute('target', '_blank');
|
||||||
node.setAttribute('rel', 'noopener noreferrer');
|
node.setAttribute('rel', 'noopener noreferrer');
|
||||||
@@ -218,7 +219,7 @@ function sanitizeHtml(html: string): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Hook: restrict inline styles to text-align only (used by TipTap)
|
// Hook: restrict inline styles to text-align only (used by TipTap)
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
articlePurify.addHook('afterSanitizeAttributes', (node) => {
|
||||||
if (node.hasAttribute('style')) {
|
if (node.hasAttribute('style')) {
|
||||||
const style = node.getAttribute('style') ?? '';
|
const style = node.getAttribute('style') ?? '';
|
||||||
const match = style.match(/text-align\s*:\s*(left|center|right|justify)/i);
|
const match = style.match(/text-align\s*:\s*(left|center|right|justify)/i);
|
||||||
@@ -230,8 +231,8 @@ function sanitizeHtml(html: string): string {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = DOMPurify.sanitize(html, SANITIZE_CONFIG);
|
const result = articlePurify.sanitize(html, SANITIZE_CONFIG);
|
||||||
DOMPurify.removeAllHooks();
|
articlePurify.removeAllHooks();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +423,7 @@ export default function NewsArticlePage() {
|
|||||||
className="overflow-hidden rounded-xl"
|
className="overflow-hidden rounded-xl"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={article.featured_image_url!}
|
src={article.featured_image_url}
|
||||||
alt={article.title}
|
alt={article.title}
|
||||||
className="h-auto w-full rounded-xl object-cover"
|
className="h-auto w-full rounded-xl object-cover"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
|
|||||||
Reference in New Issue
Block a user