Skip to content

Commit d75f864

Browse files
committed
fix: add URL sanitization to prevent XSS and redirect vulnerabilities in Thumbnails component
1 parent bf3adfc commit d75f864

File tree

1 file changed

+72
-42
lines changed

1 file changed

+72
-42
lines changed

webview-ui/src/components/common/Thumbnails.tsx

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,28 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp
3636
vscode.postMessage({ type: "openImage", text: image })
3737
}
3838

39+
// Sanitize image URL to prevent XSS and malicious redirects
40+
const sanitizeImageUrl = (url: string): string => {
41+
try {
42+
// Only allow data URLs (base64 images) and https URLs
43+
if (url.startsWith("data:image/")) {
44+
return url
45+
}
46+
47+
// For other URLs, validate they are safe
48+
const parsedUrl = new URL(url)
49+
if (parsedUrl.protocol === "https:" || parsedUrl.protocol === "http:") {
50+
return url
51+
}
52+
53+
// Reject any other protocols (javascript:, file:, etc.)
54+
return ""
55+
} catch {
56+
// Invalid URL, return empty string
57+
return ""
58+
}
59+
}
60+
3961
return (
4062
<div
4163
ref={containerRef}
@@ -46,51 +68,59 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp
4668
rowGap: 3,
4769
...style,
4870
}}>
49-
{images.map((image, index) => (
50-
<div
51-
key={index}
52-
style={{ position: "relative" }}
53-
onMouseEnter={() => setHoveredIndex(index)}
54-
onMouseLeave={() => setHoveredIndex(null)}>
55-
<img
56-
src={image}
57-
alt={`Thumbnail ${index + 1}`}
58-
style={{
59-
width: 34,
60-
height: 34,
61-
objectFit: "cover",
62-
borderRadius: 4,
63-
cursor: "pointer",
64-
}}
65-
onClick={() => handleImageClick(image)}
66-
/>
67-
{isDeletable && hoveredIndex === index && (
68-
<div
69-
onClick={() => handleDelete(index)}
71+
{images.map((image, index) => {
72+
const sanitizedUrl = sanitizeImageUrl(image)
73+
// Skip rendering if URL is invalid/unsafe
74+
if (!sanitizedUrl) {
75+
return null
76+
}
77+
78+
return (
79+
<div
80+
key={index}
81+
style={{ position: "relative" }}
82+
onMouseEnter={() => setHoveredIndex(index)}
83+
onMouseLeave={() => setHoveredIndex(null)}>
84+
<img
85+
src={sanitizedUrl}
86+
alt={`Thumbnail ${index + 1}`}
7087
style={{
71-
position: "absolute",
72-
top: -4,
73-
right: -4,
74-
width: 13,
75-
height: 13,
76-
borderRadius: "50%",
77-
backgroundColor: "var(--vscode-badge-background)",
78-
display: "flex",
79-
justifyContent: "center",
80-
alignItems: "center",
88+
width: 34,
89+
height: 34,
90+
objectFit: "cover",
91+
borderRadius: 4,
8192
cursor: "pointer",
82-
}}>
83-
<span
84-
className="codicon codicon-close"
93+
}}
94+
onClick={() => handleImageClick(image)}
95+
/>
96+
{isDeletable && hoveredIndex === index && (
97+
<div
98+
onClick={() => handleDelete(index)}
8599
style={{
86-
color: "var(--vscode-foreground)",
87-
fontSize: 10,
88-
fontWeight: "bold",
89-
}}></span>
90-
</div>
91-
)}
92-
</div>
93-
))}
100+
position: "absolute",
101+
top: -4,
102+
right: -4,
103+
width: 13,
104+
height: 13,
105+
borderRadius: "50%",
106+
backgroundColor: "var(--vscode-badge-background)",
107+
display: "flex",
108+
justifyContent: "center",
109+
alignItems: "center",
110+
cursor: "pointer",
111+
}}>
112+
<span
113+
className="codicon codicon-close"
114+
style={{
115+
color: "var(--vscode-foreground)",
116+
fontSize: 10,
117+
fontWeight: "bold",
118+
}}></span>
119+
</div>
120+
)}
121+
</div>
122+
)
123+
})}
94124
</div>
95125
)
96126
}

0 commit comments

Comments
 (0)