Skip to content

Commit e1a3d05

Browse files
committed
chore: comments from ai reviews
1 parent d82188a commit e1a3d05

File tree

5 files changed

+87
-36
lines changed

5 files changed

+87
-36
lines changed

web/src/components/FileViewer/Viewers/MarkdownViewer.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,14 @@ import React from "react";
22
import styled from "styled-components";
33

44
import { type DocRenderer } from "@cyntler/react-doc-viewer";
5-
import ReactMarkdown from "react-markdown";
5+
6+
import MarkdownRenderer from "../../MarkdownRenderer";
67

78
const Container = styled.div`
89
padding: 16px;
910
`;
1011

11-
const StyledMarkdown = styled(ReactMarkdown)`
12-
background-color: ${({ theme }) => theme.whiteBackground};
13-
a {
14-
font-size: 16px;
15-
}
16-
code {
17-
color: ${({ theme }) => theme.secondaryText};
18-
}
19-
`;
20-
21-
const MarkdownRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
12+
const MarkdownDocRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
2213
if (!currentDocument) return null;
2314
const base64String = (currentDocument.fileData as string).split(",")[1];
2415

@@ -27,12 +18,12 @@ const MarkdownRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
2718

2819
return (
2920
<Container id="md-renderer">
30-
<StyledMarkdown>{decodedData}</StyledMarkdown>
21+
<MarkdownRenderer content={decodedData} />
3122
</Container>
3223
);
3324
};
3425

35-
MarkdownRenderer.fileTypes = ["md", "text/plain"];
36-
MarkdownRenderer.weight = 1;
26+
MarkdownDocRenderer.fileTypes = ["md", "text/plain"];
27+
MarkdownDocRenderer.weight = 1;
3728

38-
export default MarkdownRenderer;
29+
export default MarkdownDocRenderer;

web/src/components/MarkdownEditor.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import InfoIcon from "svgs/icons/info-circle.svg";
2828

2929
import { sanitizeMarkdown } from "utils/markdownSanitization";
30+
import { isValidUrl } from "utils/urlValidation";
3031

3132
import { MDXEditorContainer, MDXEditorGlobalStyles } from "styles/mdxEditorTheme";
3233

@@ -103,13 +104,16 @@ const MarkdownEditor: React.FC<IMarkdownEditor> = ({
103104
markdown: value,
104105
onChange: handleChange,
105106
placeholder,
107+
suppressHtmlProcessing: true,
106108
plugins: [
107109
headingsPlugin(),
108110
listsPlugin(),
109111
quotePlugin(),
110112
thematicBreakPlugin(),
111113
markdownShortcutPlugin(),
112-
linkPlugin(),
114+
linkPlugin({
115+
validateUrl: (url) => isValidUrl(url),
116+
}),
113117
linkDialogPlugin(),
114118
tablePlugin(),
115119
toolbarPlugin({

web/src/components/MarkdownRenderer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
markdownShortcutPlugin,
1111
linkPlugin,
1212
tablePlugin,
13+
codeBlockPlugin,
1314
} from "@mdxeditor/editor";
1415

1516
import { sanitizeMarkdown } from "utils/markdownSanitization";
17+
import { isValidUrl } from "utils/urlValidation";
1618

1719
import { MDXRendererContainer } from "styles/mdxEditorTheme";
1820

@@ -33,14 +35,18 @@ const MarkdownRenderer: React.FC<IMarkdownRenderer> = ({ content, className }) =
3335
const editorProps: MDXEditorProps = {
3436
markdown: sanitizedContent,
3537
readOnly: true,
38+
suppressHtmlProcessing: true,
3639
plugins: [
3740
headingsPlugin(),
3841
listsPlugin(),
3942
quotePlugin(),
4043
thematicBreakPlugin(),
4144
markdownShortcutPlugin(),
42-
linkPlugin(),
45+
linkPlugin({
46+
validateUrl: (url) => isValidUrl(url),
47+
}),
4348
tablePlugin(),
49+
codeBlockPlugin({ defaultCodeBlockLanguage: "text" }),
4450
],
4551
};
4652

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,57 @@
11
import { sanitizeUrl } from "./urlValidation";
22

3+
const sanitizeRepeated = (text: string, pattern: RegExp, replacement: string): string => {
4+
let result = text;
5+
let previousResult;
6+
do {
7+
previousResult = result;
8+
result = result.replace(pattern, replacement);
9+
} while (result !== previousResult);
10+
return result;
11+
};
12+
313
export const sanitizeMarkdown = (markdown: string): string => {
414
if (!markdown || typeof markdown !== "string") {
515
return "";
616
}
717

8-
return markdown
9-
.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (match, text, url) => {
10-
const sanitizedUrl = sanitizeUrl(url);
11-
return `[${text}](${sanitizedUrl})`;
12-
})
13-
.replace(/<a\s+href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, (match, url, text) => {
14-
const sanitizedUrl = sanitizeUrl(url);
15-
return `[${text}](${sanitizedUrl})`;
16-
})
17-
.replace(/<script[^>]*>.*?<\/script>/gis, "")
18-
.replace(/<iframe[^>]*>.*?<\/iframe>/gis, "")
19-
.replace(/<object[^>]*>.*?<\/object>/gis, "")
20-
.replace(/<embed[^>]*\/?>/gi, "")
21-
.replace(/javascript:/gi, "")
22-
.replace(/vbscript:/gi, "")
23-
.replace(/data:/gi, "")
24-
.replace(/on\w+\s*=/gi, "");
18+
let sanitized = markdown;
19+
20+
sanitized = sanitized.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (match, text, url) => {
21+
const sanitizedUrl = sanitizeUrl(url);
22+
return `[${text}](${sanitizedUrl})`;
23+
});
24+
25+
sanitized = sanitized.replace(/<a\s+href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, (match, url, text) => {
26+
const sanitizedUrl = sanitizeUrl(url);
27+
return `[${text}](${sanitizedUrl})`;
28+
});
29+
30+
sanitized = sanitizeRepeated(sanitized, /<script[^>]*>.*?<\/script>/gis, "");
31+
sanitized = sanitizeRepeated(sanitized, /<iframe[^>]*>.*?<\/iframe>/gis, "");
32+
sanitized = sanitizeRepeated(sanitized, /<object[^>]*>.*?<\/object>/gis, "");
33+
sanitized = sanitizeRepeated(sanitized, /<embed[^>]*\/?>/gi, "");
34+
sanitized = sanitizeRepeated(sanitized, /<form[^>]*>.*?<\/form>/gis, "");
35+
sanitized = sanitizeRepeated(sanitized, /<input[^>]*\/?>/gi, "");
36+
sanitized = sanitizeRepeated(sanitized, /<button[^>]*>.*?<\/button>/gis, "");
37+
sanitized = sanitizeRepeated(sanitized, /<applet[^>]*>.*?<\/applet>/gis, "");
38+
sanitized = sanitizeRepeated(sanitized, /<audio[^>]*>.*?<\/audio>/gis, "");
39+
sanitized = sanitizeRepeated(sanitized, /<video[^>]*>.*?<\/video>/gis, "");
40+
sanitized = sanitizeRepeated(sanitized, /<svg[^>]*>.*?<\/svg>/gis, "");
41+
sanitized = sanitizeRepeated(sanitized, /<canvas[^>]*>.*?<\/canvas>/gis, "");
42+
43+
sanitized = sanitizeRepeated(sanitized, /javascript:/gi, "");
44+
sanitized = sanitizeRepeated(sanitized, /vbscript:/gi, "");
45+
46+
sanitized = sanitizeRepeated(sanitized, /on\w+\s*=/gi, "");
47+
sanitized = sanitizeRepeated(sanitized, /style\s*=/gi, "");
48+
49+
sanitized = sanitizeRepeated(sanitized, /<meta[^>]*\/?>/gi, "");
50+
sanitized = sanitizeRepeated(sanitized, /<link[^>]*\/?>/gi, "");
51+
sanitized = sanitizeRepeated(sanitized, /<base[^>]*\/?>/gi, "");
52+
53+
sanitized = sanitized.replace(/&#x[0-9a-f]+;/gi, "");
54+
sanitized = sanitized.replace(/&#[0-9]+;/gi, "");
55+
56+
return sanitized;
2557
};

web/src/utils/urlValidation.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
const DANGEROUS_PROTOCOLS = ["javascript:", "data:", "vbscript:", "file:", "about:"];
1+
const DANGEROUS_PROTOCOLS = ["javascript:", "vbscript:", "file:", "about:", "blob:", "filesystem:"];
22

33
const ALLOWED_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
44

5+
const ALLOWED_DATA_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"];
6+
7+
const isValidDataUri = (url: string): boolean => {
8+
const dataUriRegex = /^data:([^;,]+)(;base64)?,/i;
9+
const match = url.match(dataUriRegex);
10+
11+
if (!match) {
12+
return false;
13+
}
14+
15+
const mimeType = match[1].toLowerCase();
16+
return ALLOWED_DATA_TYPES.includes(mimeType);
17+
};
18+
519
export const isValidUrl = (url: string): boolean => {
620
if (!url || typeof url !== "string") {
721
return false;
@@ -13,6 +27,10 @@ export const isValidUrl = (url: string): boolean => {
1327
return false;
1428
}
1529

30+
if (trimmedUrl.startsWith("data:")) {
31+
return isValidDataUri(trimmedUrl);
32+
}
33+
1634
for (const protocol of DANGEROUS_PROTOCOLS) {
1735
if (trimmedUrl.startsWith(protocol)) {
1836
return false;

0 commit comments

Comments
 (0)