fix(code-editor): harden media preview against SVG XSS and improve a11y

Withhold the open-in-new-tab action for SVG previews. The link is a
top-level navigation to a blob URL, which inherits the app's origin, so
a user-controlled SVG containing <script> would execute as same-origin
script. Inline <img> rendering is unaffected and stays available.

Also give the icon-only header actions (open-in-new-tab, fullscreen
toggle, close) explicit aria-labels and mark their decorative SVG icons
aria-hidden, so screen readers announce each action instead of relying
on title alone.
This commit is contained in:
Haileyesus
2026-06-29 15:16:13 +03:00
parent dc3a928bed
commit 6d5ed6fdd8

View File

@@ -142,6 +142,13 @@ export default function CodeEditorMediaPreview({
// stale URL from the previous file is never rendered during a switch.
const currentUrl = url && loadedKey === sourceKey ? url : null;
// SVGs render safely inline via <img> (scripts don't execute there), but the
// open-in-new-tab link is a top-level navigation. A blob URL inherits the
// app's origin, so a user-controlled SVG with an embedded <script> would run
// as same-origin script. Withhold the new-tab action for SVGs.
const isSvg = getPreviewMimeType(file.name) === 'image/svg+xml';
const canOpenInNewTab = Boolean(currentUrl) && !isSvg;
const renderMedia = () => {
if (!currentUrl) return null;
switch (kind) {
@@ -198,15 +205,16 @@ export default function CodeEditorMediaPreview({
const headerActions = (
<div className="flex shrink-0 items-center gap-0.5">
{currentUrl && (
{canOpenInNewTab && currentUrl && (
<a
href={currentUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
aria-label={labels.openInNewTab}
title={labels.openInNewTab}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
@@ -216,14 +224,15 @@ export default function CodeEditorMediaPreview({
type="button"
onClick={onToggleFullscreen}
className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
aria-label={isFullscreen ? labels.exitFullscreen : labels.fullscreen}
title={isFullscreen ? labels.exitFullscreen : labels.fullscreen}
>
{isFullscreen ? (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9V4.5M9 9H4.5M9 9L3.5 3.5M9 15v4.5M9 15H4.5M9 15l-5.5 5.5M15 9h4.5M15 9V4.5M15 9l5.5-5.5M15 15h4.5M15 15v4.5m0-4.5l5.5 5.5" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />
</svg>
)}
@@ -233,9 +242,10 @@ export default function CodeEditorMediaPreview({
type="button"
onClick={onClose}
className="flex items-center justify-center rounded-md p-1.5 text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
aria-label={labels.close}
title={labels.close}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg aria-hidden="true" className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>