diff --git a/webview-ui/src/components/common/Thumbnails.tsx b/webview-ui/src/components/common/Thumbnails.tsx index acdf5f4295..22509df13a 100644 --- a/webview-ui/src/components/common/Thumbnails.tsx +++ b/webview-ui/src/components/common/Thumbnails.tsx @@ -36,6 +36,28 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp vscode.postMessage({ type: "openImage", text: image }) } + // Sanitize image URL to prevent XSS and malicious redirects + const sanitizeImageUrl = (url: string): string => { + try { + // Only allow data URLs (base64 images) and https URLs + if (url.startsWith("data:image/")) { + return url + } + + // For other URLs, validate they are safe + const parsedUrl = new URL(url) + if (parsedUrl.protocol === "https:" || parsedUrl.protocol === "http:") { + return url + } + + // Reject any other protocols (javascript:, file:, etc.) + return "" + } catch { + // Invalid URL, return empty string + return "" + } + } + return (
- {images.map((image, index) => ( -
setHoveredIndex(index)} - onMouseLeave={() => setHoveredIndex(null)}> - {`Thumbnail handleImageClick(image)} - /> - {isDeletable && hoveredIndex === index && ( -
handleDelete(index)} + {images.map((image, index) => { + const sanitizedUrl = sanitizeImageUrl(image) + // Skip rendering if URL is invalid/unsafe + if (!sanitizedUrl) { + return null + } + + return ( +
setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)}> + {`Thumbnail - handleImageClick(image)} + /> + {isDeletable && hoveredIndex === index && ( +
handleDelete(index)} style={{ - color: "var(--vscode-foreground)", - fontSize: 10, - fontWeight: "bold", - }}> -
- )} -
- ))} + position: "absolute", + top: -4, + right: -4, + width: 13, + height: 13, + borderRadius: "50%", + backgroundColor: "var(--vscode-badge-background)", + display: "flex", + justifyContent: "center", + alignItems: "center", + cursor: "pointer", + }}> + +
+ )} +
+ ) + })}
) }