CVE-2022-2027 (published 2022-06-09T17:15Z)

Improper Neutralization of Formula Elements in a CSV File in GitHub repository kromitgmbh/titra prior to 0.77.0.

Severity: LOW
CVSS v2 base score: 3.5 (AV:N/AC:M/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 8.0, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-1236
cwe_name: Improper Neutralization of Formula Elements in a CSV File
description: The product saves user-provided information into a Comma-Separated Value (CSV) file, but it does not neutralize or incorrectly neutralizes special elements that could be interpreted as a command when the file is opened by a spreadsheet product.
extended_description: User-provided data is often saved to traditional databases.  This data can be exported to a CSV file, which allows users to read the data using spreadsheet software such as Excel, Numbers, or Calc.  This software interprets entries beginning with '=' as formulas, which are then executed by the spreadsheet software.  The software's formula language often allows methods to access hyperlinks or the local command line, and frequently allows enough characters to invoke an entire script. Attackers can populate data fields which, when saved to a CSV file, may attempt information exfiltration or other malicious activity when automatically executed by the spreadsheet software.
			

Fix information:
repo_url: https://github.com/kromitgmbh/titra
commit_details: repo_url: https://github.com/kromitgmbh/titra
author: Fabian Kromer
author_date: 2022-06-07 15:13:20+02:00
msg: 🔒 fixes three critical stored XSS vulnerabilities thanks @saharshtapi for reporting this
🔒 fixes a formula injection vulnerability in the Excel export functionality thanks @saharshtapi for reprorting this
❗️this is an important security release, we recommend to migrate asap, all titra versions below version 0.77.0 are considered insecure for production use
repository: repo_url: https://github.com/kromitgmbh/titra
repo_name: kromitgmbh/titra
description: titra - modern open source project time tracking for freelancers and small teams
repo_language: JavaScript
owner: kromitgmbh
file_changes: filename: release
change_type: ModificationType.MODIFY
diff: @@ -1 +1 @@
-METEOR@2.7.2
+METEOR@2.7.3

programming_language: SQL
filename: versions
change_type: ModificationType.MODIFY
diff: @@ -89,7 +89,7 @@ templating-compiler@1.4.1
 templating-runtime@1.5.0
 templating-tools@1.2.2
 tracker@1.2.0
-tunguska:reactive-aggregate@1.3.7
+tunguska:reactive-aggregate@1.3.8
 ui@1.0.13
 underscore@1.0.10
 url@1.3.2

programming_language: Shell
filename: allprojectschart.js
change_type: ModificationType.MODIFY
diff: @@ -128,7 +128,7 @@ Template.allprojectschart.onRendered(() => {
               colors: ['#009688', '#455A64', '#e4e4e4'],
               height: 230,
               data: {
-                labels: templateInstance.topTasks.get().map((task) => task._id),
+                labels: templateInstance.topTasks.get().map((task) => $('<span>').text(task._id).html()),
                 datasets: [{
                   values: templateInstance.topTasks.get().map((task) => task.count),
                 }],

programming_language: JavaScript
filename: dailytimetable.js
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ import {
   numberWithUserPrecision,
   getUserSetting,
   getUserTimeUnitVerbose,
+  addToolTipToTableCell,
 } from '../../utils/frontend_helpers'
 import { i18nReady, t } from '../../utils/i18n.js'
 import { dailyTimecardMapper } from '../../utils/server_method_helpers'
@@ -68,8 +69,18 @@ Template.dailytimetable.onRendered(() => {
           width: 1,
           compareValue: (cell, keyword) => [dayjs(cell, getGlobalSetting('dateformat')).toDate(), dayjs(keyword, getGlobalSetting('dateformat')).toDate()],
         },
-        { name: t('globals.project'), editable: false, width: 2 },
-        { name: t('globals.resource'), editable: false, width: 2 },
+        {
+          name: t('globals.project'),
+          editable: false,
+          width: 2,
+          format: addToolTipToTableCell,
+        },
+        {
+          name: t('globals.resource'),
+          editable: false,
+          width: 2,
+          format: addToolTipToTableCell,
+        },
         {
           name: getUserTimeUnitVerbose(),
           editable: false,

programming_language: TypeScript
filename: detailtimetable.js
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,7 @@ function detailedDataTableMapper(entry) {
   const project = Projects.findOne({ _id: entry.projectId })
   const mapping = [project ? project.name : '',
     dayjs.utc(entry.date).format(getGlobalSetting('dateformat')),
-    entry.task,
+    entry.task.replace(/^=/, '='),
     projectUsers.findOne() ? projectUsers.findOne().users.find((elem) => elem._id === entry.userId)?.profile?.name : '']
   if (getGlobalSetting('showCustomFieldsInDetails')) {
     if (CustomFields.find({ classname: 'time_entry' }).count() > 0) {

programming_language: TypeScript
filename: periodtimetable.js
change_type: ModificationType.MODIFY
diff: @@ -11,6 +11,7 @@ import {
   numberWithUserPrecision,
   getUserSetting,
   getUserTimeUnitVerbose,
+  addToolTipToTableCell,
 } from '../../utils/frontend_helpers.js'
 import { totalHoursForPeriodMapper } from '../../utils/server_method_helpers.js'
 
@@ -61,8 +62,8 @@ Template.periodtimetable.onRendered(() => {
             .map((key) => key[1]))
       }
       const columns = [
-        { name: t('globals.project'), editable: false },
-        { name: t('globals.resource'), editable: false },
+        { name: t('globals.project'), editable: false, format: addToolTipToTableCell },
+        { name: t('globals.resource'), editable: false, format: addToolTipToTableCell },
         {
           name: getUserTimeUnitVerbose(),
           editable: false,

programming_language: TypeScript
filename: projectAccessRights.js
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@ import { FlowRouter } from 'meteor/ostrio:flow-router-extra'
 import './projectAccessRights.html'
 import { t, i18nReady } from '../../utils/i18n.js'
 import {
-  validateEmail, getGlobalSetting, showToast,
+  validateEmail, getGlobalSetting, showToast, addToolTipToTableCell,
 } from '../../utils/frontend_helpers'
 import Projects from '../../api/projects/projects.js'
 
@@ -28,6 +28,7 @@ Template.projectAccessRights.onRendered(() => {
           name: t('globals.name'),
           editable: false,
           focusable: false,
+          format: addToolTipToTableCell,
         }, {
           name: t('project.access_rights'),
           editable: false,

programming_language: TypeScript
filename: projectchart.js
change_type: ModificationType.MODIFY
diff: @@ -200,7 +200,7 @@ Template.projectchart.onRendered(() => {
                 colors: [Projects.findOne({ _id: templateInstance.data.projectId }).color || '#009688', '#66c0b8', '#e4e4e4'],
                 height: 230,
                 data: {
-                  labels: templateInstance.topTasks.get().map((task) => task._id),
+                  labels: templateInstance.topTasks.get().map((task) => $('<span>').text(task._id).html()),
                   datasets: [{
                     values: templateInstance.topTasks.get().map((task) => task.count),
                   }],

programming_language: JavaScript
filename: dashboard.js
change_type: ModificationType.MODIFY
diff: @@ -10,6 +10,7 @@ import {
   getGlobalSetting,
   timeInUserUnit,
   getUserTimeUnitVerbose,
+  addToolTipToTableCell,
 } from '../../utils/frontend_helpers'
 import './dashboard.html'
 import Timecards from '../../api/timecards/timecards'
@@ -126,7 +127,7 @@ Template.dashboard.onRendered(() => {
           const precision = getUserSetting('precision')
           for (const timecard of Timecards.find({}, { sort: { date: 1 } }).fetch()) {
             taskmap.set(
-              timecard.task.replace(/(:\S*:)/g, emojify),
+              $('<span>').text(timecard.task.replace(/(:\S*:)/g, emojify)).html(),
               taskmap.get(timecard.task.replace(/(:\S*:)/g, emojify))
                 ? Number(Number(taskmap.get(timecard.task.replace(/(:\S*:)/g, emojify))) + Number(timeInUnitHelper(timecard.hours)))
                 : Number(timeInUnitHelper(timecard.hours)),

programming_language: TypeScript
filename: frontend_helpers.js
change_type: ModificationType.MODIFY
diff: @@ -18,7 +18,8 @@ function getUserSetting(field) {
 
 function addToolTipToTableCell(value) {
   if (value) {
-    return `<span class="js-tooltip" data-bs-toggle="tooltip" data-bs-placement="left" title="${value}">${value}</span>`
+    const sanitizedValue = $('<div/>').text(value).html()
+    return `<span class="js-tooltip" data-bs-toggle="tooltip" data-bs-placement="left" title='${sanitizedValue}'>${sanitizedValue}</span>`
   }
   return ''
 }

programming_language: JavaScript
filename: server_method_helpers.js
change_type: ModificationType.MODIFY
diff: @@ -67,7 +67,8 @@ function totalHoursForPeriodMapper(entry) {
   }
   return {
     projectId: Projects.findOne({ _id: entry._id.projectId }).name,
-    userId: projectUsers.findOne().users.find((elem) => elem._id === entry._id.userId)?.profile?.name,
+    userId: projectUsers
+      .findOne().users.find((elem) => elem._id === entry._id.userId)?.profile?.name,
     totalHours,
   }
 }

programming_language: TypeScript
filename: package-lock.json
change_type: ModificationType.MODIFY
diff: @@ -793,9 +793,9 @@
       }
     },
     "dayjs": {
-      "version": "1.11.2",
-      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.2.tgz",
-      "integrity": "sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw=="
+      "version": "1.11.3",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
+      "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
     },
     "dayjs-precise-range": {
       "version": "1.0.1",
@@ -962,9 +962,9 @@
       "dev": true
     },
     "eslint": {
-      "version": "8.16.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz",
-      "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==",
+      "version": "8.17.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz",
+      "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==",
       "dev": true,
       "requires": {
         "@eslint/eslintrc": "^1.3.0",
@@ -1397,7 +1397,7 @@
     "fast-levenshtein": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
       "dev": true
     },
     "file-entry-cache": {
@@ -1485,7 +1485,7 @@
     "functional-red-black-tree": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
-      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
       "dev": true
     },
     "gauge": {
@@ -1657,7 +1657,7 @@
     "imurmurhash": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
       "dev": true
     },
     "inflight": {
@@ -1830,7 +1830,7 @@
     "isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "dev": true
     },
     "jalaali-js": {
@@ -1877,7 +1877,7 @@
     "json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
       "dev": true
     },
     "json5": {
@@ -2829,7 +2829,7 @@
     "natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
-      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "dev": true
     },
     "node-addon-api": {

programming_language: JavaScript
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "name": "titra",
-  "version": "0.76.1",
+  "version": "0.77.0",
   "private": true,
   "scripts": {
     "start": "meteor run"
@@ -19,7 +19,7 @@
     "bootstrap": "^5.1.3",
     "content-type": "^1.0.4",
     "date-holidays": "^3.16.0",
-    "dayjs": "^1.11.2",
+    "dayjs": "^1.11.3",
     "dayjs-precise-range": "^1.0.1",
     "docker-names": "^1.2.1",
     "file-saver": "^2.0.5",
@@ -46,7 +46,7 @@
   "devDependencies": {
     "@babel/core": "^7.18.2",
     "@babel/eslint-parser": "^7.18.2",
-    "eslint": "^8.16.0",
+    "eslint": "^8.17.0",
     "eslint-config-airbnb-base": "^15.0.0",
     "eslint-import-resolver-meteor": "^0.4.0",
     "eslint-plugin-i18next": "^5.2.1",

programming_language: JavaScript