mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Fix stored XSS vulnerabilities and unauthenticated auth toggle
- Sanitize project notes with DOMPurify before rendering via
dangerouslySetInnerHTML (ProjectDetailPage.tsx)
- Replace hand-rolled HTML sanitizer with DOMPurify in ProjectPageModal
to prevent attribute injection via crafted 3MF href values
- Block /api/v1/auth/setup when auth is already enabled to prevent
unauthenticated clients from disabling authentication remotely
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
X,
|
||||
@@ -93,53 +94,13 @@ export function ProjectPageModal({ archiveId, archiveName, onClose }: ProjectPag
|
||||
setEditData({});
|
||||
};
|
||||
|
||||
// Sanitize HTML content (basic XSS prevention)
|
||||
// Sanitize HTML content using DOMPurify
|
||||
const sanitizeHtml = (html: string) => {
|
||||
// Allow basic formatting tags only
|
||||
const allowed = ['p', 'br', 'b', 'strong', 'i', 'em', 'u', 'a', 'ul', 'ol', 'li', 'figure', 'img'];
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
const clean = (node: Node): string => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.textContent || '';
|
||||
}
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as Element;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
|
||||
if (!allowed.includes(tag)) {
|
||||
// Return children content without the tag
|
||||
return Array.from(el.childNodes).map(clean).join('');
|
||||
}
|
||||
|
||||
// Build allowed attributes
|
||||
let attrs = '';
|
||||
if (tag === 'a' && el.getAttribute('href')) {
|
||||
const href = el.getAttribute('href');
|
||||
if (href?.toLowerCase().startsWith('http')) {
|
||||
attrs = ` href="${href}" target="_blank" rel="noopener noreferrer"`;
|
||||
}
|
||||
}
|
||||
if (tag === 'img') {
|
||||
const src = el.getAttribute('src');
|
||||
// Only render img if it has a valid http(s) URL, otherwise skip entirely
|
||||
if (!src?.toLowerCase().startsWith('http')) {
|
||||
return ''; // Skip images without valid URLs
|
||||
}
|
||||
attrs = ` src="${src}" style="max-width: 100%; height: auto;"`;
|
||||
}
|
||||
|
||||
const children = Array.from(el.childNodes).map(clean).join('');
|
||||
|
||||
if (['br', 'img'].includes(tag)) {
|
||||
return `<${tag}${attrs} />`;
|
||||
}
|
||||
return `<${tag}${attrs}>${children}</${tag}>`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return Array.from(doc.body.childNodes).map(clean).join('');
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['p', 'br', 'b', 'strong', 'i', 'em', 'u', 'a', 'ul', 'ol', 'li', 'figure', 'img'],
|
||||
ALLOWED_ATTR: ['href', 'src', 'target', 'rel', 'style'],
|
||||
ADD_ATTR: ['target'],
|
||||
});
|
||||
};
|
||||
|
||||
const hasContent = projectPage && (
|
||||
|
||||
Reference in New Issue
Block a user