CVE-2021-25989: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: In “ifme”, versions 1.0.0 to v7.31.4 are vulnerable against stored XSS vulnerability in the markdown editor. It can be exploited by making a victim a Leader of a group which triggers the payload for them.
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: 2021-12-29T09:15Z
Repository: https://github.com/ifmeorg/ifme
Language: Other
Files changed: app/views/groups/index.html.erb, client/app/components/Avatar/index.jsx, client/app/components/Input/InputTextarea.jsx
Commit date: 2021-11-18 23:01:15 -0500
Commit message: [#2052] Fix stored XSS in Groups

Diff: diff --git a/app/views/groups/index.html.erb b/app/views/groups/index.html.erb
index 0b18bdeea..78ca0cf5a 100644
--- a/app/views/groups/index.html.erb
+++ b/app/views/groups/index.html.erb
@@ -46,24 +46,28 @@
     }) %>
   </div>
 <% end %>
 
 <% if @available_groups.any? %>
-  <div class="title"><%= t('groups.index.available_groups') %></div>
-  <div class="gridTwo">
-    <% @available_groups.each do |group| %>
-      <div class="gridTwoItemBoxLight">
-        <%= react_component('Story', html_options: html_options, props: {
-          name: group.name,
-          link: url_for(group),
-          actions: {
-            join: {
-              name: t('common.actions.join'),
-              link: group_membership_path(group_id: group.id, id: 'join'),
-              dataMethod: 'post'
+  <div class="smallMarginTop">
+    <div class="title">
+      <%= t('groups.index.available_groups') %>
+    </div>
+    <div class="gridTwo">
+      <% @available_groups.each do |group| %>
+        <div class="gridTwoItemBoxLight">
+          <%= react_component('Story', html_options: html_options, props: {
+            name: group.name,
+            link: url_for(group),
+            actions: {
+              join: {
+                name: t('common.actions.join'),
+                link: group_membership_path(group_id: group.id, id: 'join'),
+                dataMethod: 'post'
+              }
             }
-          }
-        }) %>
-      </div>
-    <% end %>
+          }) %>
+        </div>
+      <% end %>
+    </div>
   </div>
 <% end %>
diff --git a/client/app/components/Avatar/index.jsx b/client/app/components/Avatar/index.jsx
index 4b4bde298..664de20a5 100644
--- a/client/app/components/Avatar/index.jsx
+++ b/client/app/components/Avatar/index.jsx
@@ -30,11 +30,13 @@ export const Avatar = (props: Props) => {
     name, alt, src, small, large,
   } = props;
   const height = getHeight(small, large);
   return (
     <div
-      className={`avatar ${css.avatar} ${large ? css.large : ''} ${small ? css.small : ''}`}
+      className={`avatar ${css.avatar} ${large ? css.large : ''} ${
+        small ? css.small : ''
+      }`}
     >
       <LazyLoad height={height} offset={height} once tabIndex={0}>
         {src ? (
           displayImage(alt, src)
         ) : (
diff --git a/client/app/components/Input/InputTextarea.jsx b/client/app/components/Input/InputTextarea.jsx
index 2123f9bbf..f0b880d76 100644
--- a/client/app/components/Input/InputTextarea.jsx
+++ b/client/app/components/Input/InputTextarea.jsx
@@ -1,16 +1,15 @@
 // @flow
 import React, { useState, useEffect, useRef } from 'react';
+import { sanitize } from 'dompurify';
 import ReactDOMServer from 'react-dom/server';
 import { init, exec } from 'pell';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faLink } from '@fortawesome/free-solid-svg-icons';
 import css from './InputTextarea.scss';
 import inputCss from './Input.scss';
 
-// TODO (julianguyen): More tests after writing stubs for Pell editor
-
 const handleResult = (type: string) => {
   switch (type) {
     case 'link': {
       const url = window.prompt('URL');
       if (url) exec('createLink', url);
@@ -65,16 +64,16 @@ export type Props = {
 
 export function InputTextarea(props: Props) {
   const {
     id, name, value: propValue, required, hasError, myRef, dark,
   } = props;
-  const [value, setValue] = useState<string>(propValue || '');
+  const [value, setValue] = useState<string>(sanitize(propValue) || '');
   const editorRef = useRef(null);
   const editor = useRef(null);
 
   const onChange = (updatedValue: string) => {
-    setValue(updatedValue);
+    setValue(sanitize(updatedValue));
   };
 
   const onBlur = () => {
     if (required && hasError) {
       hasError(!value || value === '<p><br></p>');
@@ -93,11 +92,11 @@ export function InputTextarea(props: Props) {
   const onPaste = (e) => {
     e.preventDefault();
 
     const text = (e.originalEvent || e).clipboardData.getData('text/plain') ?? '';
 
-    document.execCommand('insertHTML', false, text);
+    document.execCommand('insertHTML', false, sanitize(text));
   };
 
   useEffect(() => {
     if (editorRef.current) {
       editor.current = init({

Vulnerable code:   <div class="title"><%= t('groups.index.available_groups') %></div>
  <div class="gridTwo">
    <% @available_groups.each do |group| %>
      <div class="gridTwoItemBoxLight">
        <%= react_component('Story', html_options: html_options, props: {
          name: group.name,
          link: url_for(group),
          actions: {
            join: {
              name: t('common.actions.join'),
              link: group_membership_path(group_id: group.id, id: 'join'),
              dataMethod: 'post'
          }
        }) %>
      </div>
    <% end %>
      className={`avatar ${css.avatar} ${large ? css.large : ''} ${small ? css.small : ''}`}
// TODO (julianguyen): More tests after writing stubs for Pell editor

  const [value, setValue] = useState<string>(propValue || '');
    setValue(updatedValue);
    document.execCommand('insertHTML', false, text);
Fixed code:   <div class="smallMarginTop">
    <div class="title">
      <%= t('groups.index.available_groups') %>
    </div>
    <div class="gridTwo">
      <% @available_groups.each do |group| %>
        <div class="gridTwoItemBoxLight">
          <%= react_component('Story', html_options: html_options, props: {
            name: group.name,
            link: url_for(group),
            actions: {
              join: {
                name: t('common.actions.join'),
                link: group_membership_path(group_id: group.id, id: 'join'),
                dataMethod: 'post'
              }
          }) %>
        </div>
      <% end %>
    </div>
      className={`avatar ${css.avatar} ${large ? css.large : ''} ${
        small ? css.small : ''
      }`}
import { sanitize } from 'dompurify';
  const [value, setValue] = useState<string>(sanitize(propValue) || '');
    setValue(sanitize(updatedValue));
    document.execCommand('insertHTML', false, sanitize(text));