CVE-2022-0526: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in GitHub repository chatwoot/chatwoot prior to 2.2.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: 6.1
Published: 2022-02-09T05:15Z
Repository: https://github.com/chatwoot/chatwoot
Language: JavaScript
Files changed: app/javascript/dashboard/components/CustomAttribute.vue, app/javascript/dashboard/helper/URLHelper.js, app/javascript/dashboard/helper/specs/URLHelper.spec.js, app/javascript/dashboard/mixins/attributeMixin.js, app/javascript/dashboard/mixins/specs/attributeMixin.spec.js
Commit date: 2022-02-07 13:04:27 +0530
Commit message: fix: Render valid urls only in custom attributes (#3921)


Diff: diff --git a/app/javascript/dashboard/components/CustomAttribute.vue b/app/javascript/dashboard/components/CustomAttribute.vue
index 6182a00cd..41773b125 100644
--- a/app/javascript/dashboard/components/CustomAttribute.vue
+++ b/app/javascript/dashboard/components/CustomAttribute.vue
@@ -61,11 +61,11 @@
           :href="value"
           target="_blank"
           rel="noopener noreferrer"
           class="value"
         >
-          {{ value || '---' }}
+          {{ urlValue }}
         </a>
         <p v-else class="value">
           {{ displayValue || '---' }}
         </p>
         <div class="action-buttons__wrap">
@@ -117,11 +117,11 @@
 <script>
 import format from 'date-fns/format';
 import { required, url } from 'vuelidate/lib/validators';
 import { BUS_EVENTS } from 'shared/constants/busEvents';
 import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
-
+import { isValidURL } from '../helper/URLHelper';
 const DATE_FORMAT = 'yyyy-MM-dd';
 
 export default {
   components: {
     MultiselectDropdown,
@@ -182,10 +182,13 @@ export default {
       return this.attributeType === 'link';
     },
     isAttributeTypeDate() {
       return this.attributeType === 'date';
     },
+    urlValue() {
+      return isValidURL(this.value) ? this.value : '---';
+    },
     notAttributeTypeCheckboxAndList() {
       return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
     },
     inputType() {
       return this.isAttributeTypeLink ? 'url' : this.attributeType;
diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js
index 503b98f8e..e229ce7e4 100644
--- a/app/javascript/dashboard/helper/URLHelper.js
+++ b/app/javascript/dashboard/helper/URLHelper.js
@@ -35,5 +35,11 @@ export const accountIdFromPathname = pathname => {
   // eslint-disable-next-line no-restricted-globals
   const isScoped = isInsideAccountScopedURLs && !isNaN(urlParam);
   const accountId = isScoped ? Number(urlParam) : '';
   return accountId;
 };
+
+export const isValidURL = value => {
+  /* eslint-disable no-useless-escape */
+  const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
+  return URL_REGEX.test(value);
+};
diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
index 8b4a5266d..ade735642 100644
--- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js
+++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
@@ -1,9 +1,10 @@
 import {
   frontendURL,
   conversationUrl,
   accountIdFromPathname,
+  isValidURL,
 } from '../URLHelper';
 
 describe('#URL Helpers', () => {
   describe('conversationUrl', () => {
     it('should return direct conversation URL if activeInbox is nil', () => {
@@ -46,6 +47,15 @@ describe('#URL Helpers', () => {
     });
     it('should return empty string if empty string is passed', () => {
       expect(accountIdFromPathname('')).toBe('');
     });
   });
+
+  describe('isValidURL', () => {
+    it('should return true if valid url is passed', () => {
+      expect(isValidURL('https://chatwoot.com')).toBe(true);
+    });
+    it('should return false if invalid url is passed', () => {
+      expect(isValidURL('alert.window')).toBe(false);
+    });
+  });
 });
diff --git a/app/javascript/dashboard/mixins/attributeMixin.js b/app/javascript/dashboard/mixins/attributeMixin.js
index d861ec059..6348067b2 100644
--- a/app/javascript/dashboard/mixins/attributeMixin.js
+++ b/app/javascript/dashboard/mixins/attributeMixin.js
@@ -1,7 +1,7 @@
 import { mapGetters } from 'vuex';
-
+import { isValidURL } from '../helper/URLHelper';
 export default {
   computed: {
     ...mapGetters({
       currentChat: 'getSelectedChat',
       accountId: 'getCurrentAccountId',
@@ -61,20 +61,15 @@ export default {
     isAttributeNumber(attributeValue) {
       return (
         Number.isInteger(Number(attributeValue)) && Number(attributeValue) > 0
       );
     },
-    isAttributeLink(attributeValue) {
-      /* eslint-disable no-useless-escape */
-      const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
-      return URL_REGEX.test(attributeValue);
-    },
     attributeDisplayType(attributeValue) {
       if (this.isAttributeNumber(attributeValue)) {
         return 'number';
       }
-      if (this.isAttributeLink(attributeValue)) {
+      if (isValidURL(attributeValue)) {
         return 'link';
       }
       return 'text';
     },
   },
diff --git a/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js b/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js
index 0c98f45c8..9a5efa463 100644
--- a/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js
+++ b/app/javascript/dashboard/mixins/specs/attributeMixin.spec.js
@@ -90,22 +90,10 @@ describe('attributeMixin', () => {
       wrapper.vm.attributeDisplayType('https://www.chatwoot.com/pricing')
     ).toBe('link');
     expect(wrapper.vm.attributeDisplayType(9988)).toBe('number');
   });
 
-  it('return true if link is passed', () => {
-    const Component = {
-      render() {},
-      title: 'TestComponent',
-      mixins: [attributeMixin],
-    };
-    const wrapper = shallowMount(Component, { store, localVue });
-    expect(wrapper.vm.isAttributeLink('https://www.chatwoot.com/pricing')).toBe(
-      true
-    );
-  });
-
   it('return true if number is passed', () => {
     const Component = {
       render() {},
       title: 'TestComponent',
       mixins: [attributeMixin],

Vulnerable code:           {{ value || '---' }}


    isAttributeLink(attributeValue) {
      /* eslint-disable no-useless-escape */
      const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
      return URL_REGEX.test(attributeValue);
    },
      if (this.isAttributeLink(attributeValue)) {
  it('return true if link is passed', () => {
    const Component = {
      render() {},
      title: 'TestComponent',
      mixins: [attributeMixin],
    };
    const wrapper = shallowMount(Component, { store, localVue });
    expect(wrapper.vm.isAttributeLink('https://www.chatwoot.com/pricing')).toBe(
      true
    );
  });

Fixed code:           {{ urlValue }}
import { isValidURL } from '../helper/URLHelper';
    urlValue() {
      return isValidURL(this.value) ? this.value : '---';
    },

export const isValidURL = value => {
  /* eslint-disable no-useless-escape */
  const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
  return URL_REGEX.test(value);
};
  isValidURL,

  describe('isValidURL', () => {
    it('should return true if valid url is passed', () => {
      expect(isValidURL('https://chatwoot.com')).toBe(true);
    });
    it('should return false if invalid url is passed', () => {
      expect(isValidURL('alert.window')).toBe(false);
    });
  });
import { isValidURL } from '../helper/URLHelper';
      if (isValidURL(attributeValue)) {