Harden security controls from REPORT findings
This commit is contained in:
@@ -19,6 +19,47 @@ import type { DmsDocument, DmsDocumentDetail } from '../types';
|
||||
import PathInput from './PathInput';
|
||||
import TagInput from './TagInput';
|
||||
|
||||
const SAFE_IMAGE_PREVIEW_MIME_TYPES = new Set<string>([
|
||||
'image/bmp',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
]);
|
||||
|
||||
const SAFE_IFRAME_PREVIEW_MIME_TYPES = new Set<string>([
|
||||
'application/json',
|
||||
'application/pdf',
|
||||
'text/csv',
|
||||
'text/markdown',
|
||||
'text/plain',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Normalizes MIME values by stripping parameters and lowercasing for stable comparison.
|
||||
*/
|
||||
function normalizeMimeType(mimeType: string | null | undefined): string {
|
||||
if (!mimeType) {
|
||||
return '';
|
||||
}
|
||||
return mimeType.split(';')[0]?.trim().toLowerCase() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether a MIME type is safe to render as an image preview.
|
||||
*/
|
||||
function isSafeImagePreviewMimeType(mimeType: string): boolean {
|
||||
return SAFE_IMAGE_PREVIEW_MIME_TYPES.has(mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves whether a MIME type is safe to render inside a sandboxed iframe preview.
|
||||
*/
|
||||
function isSafeIframePreviewMimeType(mimeType: string): boolean {
|
||||
return SAFE_IFRAME_PREVIEW_MIME_TYPES.has(mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines props for the selected document viewer panel.
|
||||
*/
|
||||
@@ -60,6 +101,30 @@ export default function DocumentViewer({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const previewObjectUrlRef = useRef<string | null>(null);
|
||||
|
||||
/**
|
||||
* Resolves normalized MIME type used by preview safety checks.
|
||||
*/
|
||||
const previewMimeType = useMemo(() => normalizeMimeType(document?.mime_type), [document?.mime_type]);
|
||||
|
||||
/**
|
||||
* Resolves whether selected document should render as a safe image element in preview.
|
||||
*/
|
||||
const isImageDocument = useMemo(() => {
|
||||
return isSafeImagePreviewMimeType(previewMimeType);
|
||||
}, [previewMimeType]);
|
||||
|
||||
/**
|
||||
* Resolves whether selected document should render in sandboxed iframe preview.
|
||||
*/
|
||||
const canRenderIframePreview = useMemo(() => {
|
||||
return isSafeIframePreviewMimeType(previewMimeType);
|
||||
}, [previewMimeType]);
|
||||
|
||||
/**
|
||||
* Resolves whether selected document supports any inline preview mode.
|
||||
*/
|
||||
const canRenderInlinePreview = isImageDocument || canRenderIframePreview;
|
||||
|
||||
/**
|
||||
* Syncs editable metadata fields whenever selection changes.
|
||||
*/
|
||||
@@ -100,6 +165,12 @@ export default function DocumentViewer({
|
||||
setIsLoadingPreview(false);
|
||||
return;
|
||||
}
|
||||
if (!canRenderInlinePreview) {
|
||||
revokePreviewObjectUrl();
|
||||
setPreviewObjectUrl(null);
|
||||
setIsLoadingPreview(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setIsLoadingPreview(true);
|
||||
@@ -131,7 +202,7 @@ export default function DocumentViewer({
|
||||
cancelled = true;
|
||||
revokePreviewObjectUrl();
|
||||
};
|
||||
}, [document?.id]);
|
||||
}, [document?.id, canRenderInlinePreview]);
|
||||
|
||||
/**
|
||||
* Refreshes editable metadata from list updates only while form is clean.
|
||||
@@ -183,16 +254,6 @@ export default function DocumentViewer({
|
||||
};
|
||||
}, [document?.id]);
|
||||
|
||||
/**
|
||||
* Resolves whether selected document should render as an image element in preview.
|
||||
*/
|
||||
const isImageDocument = useMemo(() => {
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
return document.mime_type.startsWith('image/');
|
||||
}, [document]);
|
||||
|
||||
/**
|
||||
* Extracts provider/transcription errors from document metadata for user visibility.
|
||||
*/
|
||||
@@ -482,11 +543,22 @@ export default function DocumentViewer({
|
||||
{previewObjectUrl ? (
|
||||
isImageDocument ? (
|
||||
<img src={previewObjectUrl} alt={document.original_filename} />
|
||||
) : canRenderIframePreview ? (
|
||||
<iframe
|
||||
src={previewObjectUrl}
|
||||
title={document.original_filename}
|
||||
sandbox=""
|
||||
referrerPolicy="no-referrer"
|
||||
allow="clipboard-read 'none'; clipboard-write 'none'; geolocation 'none'; microphone 'none'; camera 'none'; payment 'none'; usb 'none'; fullscreen 'none'"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<iframe src={previewObjectUrl} title={document.original_filename} />
|
||||
<p className="small">Preview blocked for this file type. Download to inspect safely.</p>
|
||||
)
|
||||
) : isLoadingPreview ? (
|
||||
<p className="small">Loading preview...</p>
|
||||
) : !canRenderInlinePreview ? (
|
||||
<p className="small">Preview blocked for this file type. Download to inspect safely.</p>
|
||||
) : (
|
||||
<p className="small">Preview unavailable for this document.</p>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user