Skip to content

Commit e242cea

Browse files
feat: secure image component
1 parent 902e59f commit e242cea

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React, { useState } from "react";
2+
import styled from "styled-components";
3+
import {
4+
lightGray,
5+
vscBackground,
6+
vscButtonBackground,
7+
vscButtonForeground,
8+
vscForeground,
9+
vscInputBorder,
10+
} from "../";
11+
12+
const ImagePlaceholder = styled.div`
13+
border: 1px solid ${vscInputBorder};
14+
border-radius: 4px;
15+
padding: 12px;
16+
margin: 8px 0;
17+
background-color: ${vscBackground};
18+
display: inline-block;
19+
max-width: 100%;
20+
`;
21+
22+
const WarningText = styled.div`
23+
color: ${lightGray};
24+
font-size: 12px;
25+
margin-bottom: 8px;
26+
`;
27+
28+
const UrlDisplay = styled.div`
29+
font-family: var(--vscode-editor-font-family);
30+
font-size: 12px;
31+
color: ${vscForeground};
32+
word-break: break-all;
33+
margin: 8px 0;
34+
padding: 8px;
35+
background-color: rgba(0, 0, 0, 0.1);
36+
border-radius: 3px;
37+
`;
38+
39+
const QueryParamsDisplay = styled.div`
40+
font-family: var(--vscode-editor-font-family);
41+
font-size: 11px;
42+
color: ${vscForeground};
43+
margin: 8px 0;
44+
padding: 8px;
45+
background-color: rgba(128, 128, 128, 0.1);
46+
border-radius: 3px;
47+
border: 1px solid ${lightGray};
48+
`;
49+
50+
const LoadButton = styled.button`
51+
background-color: ${vscButtonBackground};
52+
color: ${vscButtonForeground};
53+
border: 1px solid ${vscInputBorder};
54+
padding: 6px 12px;
55+
border-radius: 3px;
56+
cursor: pointer;
57+
font-size: 12px;
58+
margin-top: 8px;
59+
60+
&:hover {
61+
opacity: 0.9;
62+
}
63+
64+
&:active {
65+
transform: translateY(1px);
66+
}
67+
`;
68+
69+
const ImageContainer = styled.div`
70+
max-width: 100%;
71+
display: inline-block;
72+
73+
img {
74+
max-width: 100%;
75+
height: auto;
76+
display: block;
77+
}
78+
`;
79+
80+
interface SecureImageComponentProps {
81+
src?: string;
82+
alt?: string;
83+
title?: string;
84+
className?: string;
85+
}
86+
87+
export const SecureImageComponent: React.FC<SecureImageComponentProps> = ({
88+
src,
89+
alt,
90+
title,
91+
className,
92+
}) => {
93+
const [showImage, setShowImage] = useState(false);
94+
const [imageError, setImageError] = useState(false);
95+
96+
if (!src) {
97+
return <span>[Invalid image: no source]</span>;
98+
}
99+
100+
// Parse URL to check for query parameters
101+
let queryParams: Record<string, string> = {};
102+
let hasQueryParams = false;
103+
104+
try {
105+
const url = new URL(src, window.location.href);
106+
const params = new URLSearchParams(url.search);
107+
params.forEach((value, key) => {
108+
queryParams[key] = value;
109+
hasQueryParams = true;
110+
});
111+
} catch (e) {
112+
// If URL parsing fails, treat src as a relative path
113+
const queryIndex = src.indexOf('?');
114+
if (queryIndex > -1) {
115+
hasQueryParams = true;
116+
const params = new URLSearchParams(src.substring(queryIndex));
117+
params.forEach((value, key) => {
118+
queryParams[key] = value;
119+
});
120+
}
121+
}
122+
123+
if (showImage && !imageError) {
124+
return (
125+
<ImageContainer className={className}>
126+
<img
127+
src={src}
128+
alt={alt || ""}
129+
title={title}
130+
onError={() => {
131+
setImageError(true);
132+
setShowImage(false);
133+
}}
134+
/>
135+
</ImageContainer>
136+
);
137+
}
138+
139+
return (
140+
<ImagePlaceholder>
141+
<WarningText>
142+
Image blocked for security. Click to load if you trust the source.
143+
</WarningText>
144+
145+
<UrlDisplay>
146+
<strong>URL:</strong> {src}
147+
</UrlDisplay>
148+
149+
{hasQueryParams && (
150+
<QueryParamsDisplay>
151+
<strong>Warning: URL contains query parameters:</strong>
152+
<pre style={{ margin: "4px 0", fontSize: "11px" }}>
153+
{JSON.stringify(queryParams, null, 2)}
154+
</pre>
155+
</QueryParamsDisplay>
156+
)}
157+
158+
{imageError && (
159+
<div style={{ color: lightGray, fontSize: "12px", marginTop: "8px" }}>
160+
Failed to load image. The URL may be invalid or inaccessible.
161+
</div>
162+
)}
163+
164+
<LoadButton onClick={() => setShowImage(true)}>
165+
Load Image {hasQueryParams && "(Contains Parameters)"}
166+
</LoadButton>
167+
</ImagePlaceholder>
168+
);
169+
};

gui/src/components/StyledMarkdownPreview/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import "./katex.css";
2323
import "./markdown.css";
2424
import MermaidBlock from "./MermaidBlock";
2525
import { rehypeHighlightPlugin } from "./rehypeHighlightPlugin";
26+
import { SecureImageComponent } from "./SecureImageComponent";
2627
import { StepContainerPreToolbar } from "./StepContainerPreToolbar";
2728
import SymbolLink from "./SymbolLink";
2829
import { SyntaxHighlightedPre } from "./SyntaxHighlightedPre";
@@ -346,6 +347,16 @@ const StyledMarkdownPreview = memo(function StyledMarkdownPreview(
346347
}
347348
return <code {...codeProps}>{codeProps.children}</code>;
348349
},
350+
img: ({ ...imgProps }) => {
351+
return (
352+
<SecureImageComponent
353+
src={imgProps.src}
354+
alt={imgProps.alt}
355+
title={imgProps.title}
356+
className={imgProps.className}
357+
/>
358+
);
359+
},
349360
},
350361
},
351362
});

0 commit comments

Comments
 (0)