From 48670c461d8db2bb6828b98033ffca851501a52e Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Tue, 22 Dec 2020 14:14:26 +0100
Subject: [PATCH] feat: markdown XSS protection, better dropdown UX

---
 package-lock.json                            | 14 +++++++++++++
 package.json                                 |  5 +++--
 src/components/annoucements/Announcement.jsx |  6 +++++-
 src/components/mde/MarkdownEditor.jsx        | 12 +++--------
 src/components/posts/Post.jsx                | 15 +++++++++-----
 src/components/posts/PostList.jsx            |  2 +-
 src/markdown.js                              | 21 ++++++++++++++++++++
 src/pages/Home.jsx                           |  2 +-
 src/utils.js                                 |  3 +++
 src/ws/handlers/posts.js                     |  4 ++++
 typings/cf2021.d.ts                          |  1 +
 11 files changed, 66 insertions(+), 19 deletions(-)
 create mode 100644 src/markdown.js

diff --git a/package-lock.json b/package-lock.json
index ae8c27a..039a56e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4458,6 +4458,11 @@
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
       "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
     },
+    "cssfilter": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
+      "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
+    },
     "cssnano": {
       "version": "4.1.10",
       "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
@@ -14977,6 +14982,15 @@
         "@babel/runtime-corejs3": "^7.8.3"
       }
     },
+    "xss": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz",
+      "integrity": "sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==",
+      "requires": {
+        "commander": "^2.20.3",
+        "cssfilter": "0.0.10"
+      }
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index fe1ec70..ce86b58 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,8 @@
     "react-scripts": "3.4.3",
     "showdown": "^1.9.1",
     "unfetch": "^4.2.0",
-    "wait-queue": "^1.1.4"
+    "wait-queue": "^1.1.4",
+    "xss": "^1.0.8"
   },
   "scripts": {
     "start": "react-scripts start",
@@ -58,7 +59,7 @@
               "^@?\\w"
             ],
             [
-              "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|ws)(/.*|$)"
+              "^(api|actions|config|hooks|components|containers|pages|utils|stores|keycloak|markdown|ws)(/.*|$)"
             ],
             [
               "^(test-utils)(/.*|$)"
diff --git a/src/components/annoucements/Announcement.jsx b/src/components/annoucements/Announcement.jsx
index 95973cc..0852cad 100644
--- a/src/components/annoucements/Announcement.jsx
+++ b/src/components/annoucements/Announcement.jsx
@@ -95,7 +95,11 @@ const Announcement = ({
           )}
         </div>
         {canRunActions && (
-          <DropdownMenu right triggerIconClass="ico--dots-three-horizontal">
+          <DropdownMenu
+            right
+            className="pl-4"
+            triggerIconClass="ico--dots-three-horizontal"
+          >
             {showEdit && (
               <DropdownMenuItem
                 onClick={onEdit}
diff --git a/src/components/mde/MarkdownEditor.jsx b/src/components/mde/MarkdownEditor.jsx
index 88a74fe..f2ff089 100644
--- a/src/components/mde/MarkdownEditor.jsx
+++ b/src/components/mde/MarkdownEditor.jsx
@@ -1,18 +1,12 @@
 import React, { useState } from "react";
 import ReactMde from "react-mde";
 import classNames from "classnames";
-import Showdown from "showdown";
+
+import { markdownConverter } from "markdown";
 
 import "react-mde/lib/styles/css/react-mde-toolbar.css";
 import "./MarkdownEditor.css";
 
-const converter = new Showdown.Converter({
-  tables: true,
-  simplifiedAutoLink: true,
-  strikethrough: true,
-  tasklists: true,
-});
-
 const MarkdownEditor = ({
   value,
   onChange,
@@ -47,7 +41,7 @@ const MarkdownEditor = ({
         selectedTab={selectedTab}
         onTabChange={setSelectedTab}
         generateMarkdownPreview={(markdown) =>
-          Promise.resolve(converter.makeHtml(markdown))
+          Promise.resolve(markdownConverter.makeHtml(markdown))
         }
         classes={classes}
         l18n={l18n}
diff --git a/src/components/posts/Post.jsx b/src/components/posts/Post.jsx
index 72ed992..3a57d1e 100644
--- a/src/components/posts/Post.jsx
+++ b/src/components/posts/Post.jsx
@@ -135,6 +135,10 @@ const Post = ({
   const showHideAction = !archived;
   const showArchiveAction = !archived;
 
+  const htmlContent = {
+    __html: content,
+  };
+
   return (
     <div className={wrapperClassName} ref={ref}>
       <img
@@ -164,7 +168,7 @@ const Post = ({
                 {labels}
               </div>
             </div>
-            <div className="flex items-center space-x-4">
+            <div className="flex items-center">
               <Thumbs
                 likes={ranking.likes}
                 dislikes={ranking.dislikes}
@@ -174,7 +178,7 @@ const Post = ({
                 myVote={ranking.myVote}
               />
               {canRunActions && (
-                <DropdownMenu right>
+                <DropdownMenu right className="pl-4">
                   {showAnnounceAction && (
                     <DropdownMenuItem
                       onClick={onAnnounceProcedureProposal}
@@ -239,9 +243,10 @@ const Post = ({
         <div className="flex lg:hidden flex-row flex-wrap my-2 space-x-2">
           {labels}
         </div>
-        <p className="text-sm lg:text-base text-black leading-normal">
-          {content}
-        </p>
+        <div
+          className="text-sm lg:text-base text-black leading-normal content-block"
+          dangerouslySetInnerHTML={htmlContent}
+        ></div>
       </div>
     </div>
   );
diff --git a/src/components/posts/PostList.jsx b/src/components/posts/PostList.jsx
index c8143ec..da1ad33 100644
--- a/src/components/posts/PostList.jsx
+++ b/src/components/posts/PostList.jsx
@@ -56,7 +56,7 @@ const PostList = ({
             author={item.author}
             type={item.type}
             state={item.state}
-            content={item.content}
+            content={item.contentHtml}
             ranking={item.ranking}
             historyLog={item.historyLog}
             modified={item.modified}
diff --git a/src/markdown.js b/src/markdown.js
new file mode 100644
index 0000000..559427c
--- /dev/null
+++ b/src/markdown.js
@@ -0,0 +1,21 @@
+import Showdown from "showdown";
+import xss from "xss";
+
+const xssFilter = (converter) => [
+  {
+    type: "output",
+    filter: (text) => xss(text),
+  },
+];
+
+export const markdownConverter = new Showdown.Converter({
+  tables: true,
+  simplifiedAutoLink: true,
+  strikethrough: true,
+  tasklists: true,
+  omitExtraWLInCodeBlocks: true,
+  noHeaderId: true,
+  headerLevelStart: 2,
+  openLinksInNewWindow: true,
+  extensions: [xssFilter],
+});
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index c3740ac..6e15e30 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -151,7 +151,7 @@ const Home = () => {
             <h1 className="head-alt-md lg:head-alt-lg mb-0">
               Bod č. {programEntry.number}: {programEntry.title}
             </h1>
-            <DropdownMenu right triggerSize="lg">
+            <DropdownMenu right triggerSize="lg" className="pl-4">
               <DropdownMenuItem
                 onClick={() => setShowProgramEditModal(true)}
                 icon="ico--edit-pencil"
diff --git a/src/utils.js b/src/utils.js
index 83fb400..a35571e 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -3,6 +3,8 @@ import pick from "lodash/pick";
 import property from "lodash/property";
 import values from "lodash/values";
 
+import { markdownConverter } from "markdown";
+
 /**
  * Filter & sort collection of posts.
  * @param {CF2021.PostStoreFilters} filters
@@ -120,6 +122,7 @@ export const announcementTypeMappingRev = {
 export const parseRawPost = (rawPost) => {
   const post = {
     ...pick(rawPost, ["id", "content", "author"]),
+    contentHtml: markdownConverter.makeHtml(rawPost.content),
     datetime: new Date(rawPost.datetime),
     historyLog: rawPost.history_log,
     ranking: {
diff --git a/src/ws/handlers/posts.js b/src/ws/handlers/posts.js
index 3c07ed0..1eb6dc7 100644
--- a/src/ws/handlers/posts.js
+++ b/src/ws/handlers/posts.js
@@ -1,5 +1,6 @@
 import has from "lodash/has";
 
+import { markdownConverter } from "markdown";
 import { PostStore } from "stores";
 import { parseRawPost, postsStateMapping, updateWindowPosts } from "utils";
 
@@ -24,6 +25,9 @@ export const handlePostChanged = (payload) => {
     if (state.items[payload.id]) {
       if (has(payload, "content")) {
         state.items[payload.id].content = payload.content;
+        state.items[payload.id].contentHtml = markdownConverter.makeHtml(
+          payload.content
+        );
         state.items[payload.id].modified = true;
       }
 
diff --git a/typings/cf2021.d.ts b/typings/cf2021.d.ts
index 7c0d201..fbed0e0 100644
--- a/typings/cf2021.d.ts
+++ b/typings/cf2021.d.ts
@@ -80,6 +80,7 @@ declare namespace CF2021 {
     };
     type: PostType;
     content: string;
+    contentHtml: string;
     ranking: {
       score: number;
       likes: number;
-- 
GitLab