mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-01 10:02:57 +08:00
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:
@@ -142,6 +142,13 @@ export default function CodeEditorMediaPreview({
|
|||||||
// stale URL from the previous file is never rendered during a switch.
|
// stale URL from the previous file is never rendered during a switch.
|
||||||
const currentUrl = url && loadedKey === sourceKey ? url : null;
|
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 = () => {
|
const renderMedia = () => {
|
||||||
if (!currentUrl) return null;
|
if (!currentUrl) return null;
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
@@ -198,15 +205,16 @@ export default function CodeEditorMediaPreview({
|
|||||||
|
|
||||||
const headerActions = (
|
const headerActions = (
|
||||||
<div className="flex shrink-0 items-center gap-0.5">
|
<div className="flex shrink-0 items-center gap-0.5">
|
||||||
{currentUrl && (
|
{canOpenInNewTab && currentUrl && (
|
||||||
<a
|
<a
|
||||||
href={currentUrl}
|
href={currentUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
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}
|
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" />
|
<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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
@@ -216,14 +224,15 @@ export default function CodeEditorMediaPreview({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onToggleFullscreen}
|
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"
|
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}
|
title={isFullscreen ? labels.exitFullscreen : labels.fullscreen}
|
||||||
>
|
>
|
||||||
{isFullscreen ? (
|
{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" />
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<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" />
|
<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>
|
</svg>
|
||||||
)}
|
)}
|
||||||
@@ -233,9 +242,10 @@ export default function CodeEditorMediaPreview({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
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"
|
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}
|
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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user