CVE-2022-4695: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository usememos/memos prior to 0.9.0.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2022-12-27T15:15Z
Repository: https://github.com/usememos/memos
Language: TypeScript
Files changed: web/src/components/SearchBar.tsx, web/src/labs/highlighter/index.ts, web/src/labs/marked/parser/Bold.ts, web/src/labs/marked/parser/BoldEmphasis.ts, web/src/labs/marked/parser/Emphasis.ts, web/src/labs/marked/parser/Link.ts
Commit date: 2022-12-23 19:17:33 +0800
Commit message: chore: add escape to prevent XSS (#833)


Diff: diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx
index 818fc61..18bfc37 100644
--- a/web/src/components/SearchBar.tsx
+++ b/web/src/components/SearchBar.tsx
@@ -36,11 +36,11 @@ const SearchBar = () => {
   }, []);
 
   useEffect(() => {
     const text = locationStore.getState().query.text;
     setQueryText(text === undefined ? "" : text);
-  }, [locationStore.getState().query.text]);
+  }, [locationStore.state.query.text]);
 
   const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => {
     const { type: prevType } = locationStore.getState().query ?? {};
     if (type === prevType) {
       type = undefined;
diff --git a/web/src/labs/highlighter/index.ts b/web/src/labs/highlighter/index.ts
index 985f8ac..d18e6cb 100644
--- a/web/src/labs/highlighter/index.ts
+++ b/web/src/labs/highlighter/index.ts
@@ -1,8 +1,6 @@
-const escapeRegExp = (str: string): string => {
-  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-};
+import { escape } from "lodash";
 
 const walkthroughNodeWithKeyword = (node: HTMLElement, keyword: string) => {
   if (node.nodeType === 3) {
     const span = document.createElement("span");
     span.innerHTML = node.nodeValue?.replace(new RegExp(keyword, "g"), `<mark>${keyword}</mark>`) ?? "";
@@ -17,10 +15,10 @@ const walkthroughNodeWithKeyword = (node: HTMLElement, keyword: string) => {
 
 export const highlightWithWord = (html: string, keyword?: string): string => {
   if (!keyword) {
     return html;
   }
-  keyword = escapeRegExp(keyword);
+  keyword = escape(keyword);
   const wrap = document.createElement("div");
-  wrap.innerHTML = html;
+  wrap.innerHTML = escape(html);
   return walkthroughNodeWithKeyword(wrap, keyword);
 };
diff --git a/web/src/labs/marked/parser/Bold.ts b/web/src/labs/marked/parser/Bold.ts
index 578a6ed..041640f 100644
--- a/web/src/labs/marked/parser/Bold.ts
+++ b/web/src/labs/marked/parser/Bold.ts
@@ -1,5 +1,6 @@
+import { escape } from "lodash";
 import { marked } from "..";
 import Link from "./Link";
 
 export const BOLD_REG = /\*\*(.+?)\*\*/;
 
@@ -12,11 +13,11 @@ const renderer = (rawStr: string): string => {
   const matchResult = matcher(rawStr);
   if (!matchResult) {
     return rawStr;
   }
 
-  const parsedContent = marked(matchResult[1], [], [Link]);
+  const parsedContent = marked(escape(matchResult[1]), [], [Link]);
   return `<strong>${parsedContent}</strong>`;
 };
 
 export default {
   name: "bold",
diff --git a/web/src/labs/marked/parser/BoldEmphasis.ts b/web/src/labs/marked/parser/BoldEmphasis.ts
index 0fdf37e..f434da5 100644
--- a/web/src/labs/marked/parser/BoldEmphasis.ts
+++ b/web/src/labs/marked/parser/BoldEmphasis.ts
@@ -1,5 +1,6 @@
+import { escape } from "lodash";
 import { marked } from "..";
 import Link from "./Link";
 
 export const BOLD_EMPHASIS_REG = /\*\*\*(.+?)\*\*\*/;
 
@@ -12,11 +13,11 @@ const renderer = (rawStr: string): string => {
   const matchResult = matcher(rawStr);
   if (!matchResult) {
     return rawStr;
   }
 
-  const parsedContent = marked(matchResult[1], [], [Link]);
+  const parsedContent = marked(escape(matchResult[1]), [], [Link]);
   return `<strong><em>${parsedContent}</em></strong>`;
 };
 
 export default {
   name: "bold emphasis",
diff --git a/web/src/labs/marked/parser/Emphasis.ts b/web/src/labs/marked/parser/Emphasis.ts
index e339bf3..7dd12df 100644
--- a/web/src/labs/marked/parser/Emphasis.ts
+++ b/web/src/labs/marked/parser/Emphasis.ts
@@ -1,5 +1,6 @@
+import { escape } from "lodash";
 import { marked } from "..";
 import Link from "./Link";
 
 export const EMPHASIS_REG = /\*(.+?)\*/;
 
@@ -12,11 +13,11 @@ const renderer = (rawStr: string): string => {
   const matchResult = matcher(rawStr);
   if (!matchResult) {
     return rawStr;
   }
 
-  const parsedContent = marked(matchResult[1], [], [Link]);
+  const parsedContent = marked(escape(matchResult[1]), [], [Link]);
   return `<em>${parsedContent}</em>`;
 };
 
 export default {
   name: "emphasis",
diff --git a/web/src/labs/marked/parser/Link.ts b/web/src/labs/marked/parser/Link.ts
index 7b9ac4f..6bfc0f8 100644
--- a/web/src/labs/marked/parser/Link.ts
+++ b/web/src/labs/marked/parser/Link.ts
@@ -15,11 +15,11 @@ const matcher = (rawStr: string) => {
 const renderer = (rawStr: string): string => {
   const matchResult = matcher(rawStr);
   if (!matchResult) {
     return rawStr;
   }
-  const parsedContent = marked(matchResult[1], [], [InlineCode, BoldEmphasis, Emphasis, Bold]);
+  const parsedContent = marked(escape(matchResult[1]), [], [InlineCode, BoldEmphasis, Emphasis, Bold]);
   return `<a class='link' target='_blank' rel='noreferrer' href='${escape(matchResult[2])}'>${parsedContent}</a>`;
 };
 
 export default {
   name: "link",

Vulnerable code:   }, [locationStore.getState().query.text]);
const escapeRegExp = (str: string): string => {
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
  keyword = escapeRegExp(keyword);
  wrap.innerHTML = html;
  const parsedContent = marked(matchResult[1], [], [Link]);
  const parsedContent = marked(matchResult[1], [], [Link]);
  const parsedContent = marked(matchResult[1], [], [Link]);
  const parsedContent = marked(matchResult[1], [], [InlineCode, BoldEmphasis, Emphasis, Bold]);
Fixed code:   }, [locationStore.state.query.text]);
import { escape } from "lodash";
  keyword = escape(keyword);
  wrap.innerHTML = escape(html);
import { escape } from "lodash";
  const parsedContent = marked(escape(matchResult[1]), [], [Link]);
import { escape } from "lodash";
  const parsedContent = marked(escape(matchResult[1]), [], [Link]);
import { escape } from "lodash";
  const parsedContent = marked(escape(matchResult[1]), [], [Link]);
  const parsedContent = marked(escape(matchResult[1]), [], [InlineCode, BoldEmphasis, Emphasis, Bold]);