CVE-2020-15255: Improper Neutralization of Formula Elements in a CSV File (CWE-1236)
value: In Anuko Time Tracker before verion 1.19.23.5325, due to not properly filtered user input a CSV export of a report could contain cells that are treated as formulas by spreadsheet software (for example, when a cell value starts with an equal sign). This is fixed in version 1.19.23.5325.
CWE 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.
CVSS v3 base score: 7.3
Published: 2020-10-16T17:15Z
Repository: https://github.com/anuko/timetracker
Language: PHP
Files changed: WEB-INF/lib/common.lib.php, initialize.php, tofile.php
Commit date: 2020-10-11 22:25:28 +0000
Commit message: Security fix for csv injections.

Diff: diff --git a/WEB-INF/lib/common.lib.php b/WEB-INF/lib/common.lib.php
index 725f343c..e4d4a38f 100644
--- a/WEB-INF/lib/common.lib.php
+++ b/WEB-INF/lib/common.lib.php
@@ -457,5 +457,14 @@ function ttRandomString($length = 32) {
   for ($i = 0; $i < $length; $i++) {
     $randomString .= $characters[rand(0, $charactersLength - 1)];
   }
   return $randomString;
 }
+
+// ttNeutralizeForCsv neutralizes user input for export to CSV files
+// by removing =, +, -, and @ characters from the beginning of cell values.
+// This mitigates a risk of CSV injection, see https://owasp.org/www-community/attacks/CSV_Injection
+// Additionally, it replaces each quote character with a double quote.
+function ttNeutralizeForCsv($val) {
+  $result = ltrim($val, '=+-@');
+  return str_replace('"', '""', $result);
+}
diff --git a/initialize.php b/initialize.php
index afde5964..93d01917 100644
--- a/initialize.php
+++ b/initialize.php
@@ -35,11 +35,11 @@ error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT); // & ~E_DEPRECATED);
 
 // Disable displaying errors on screen.
 ini_set('display_errors', 'Off');
 
 // require_once('init_auth.php');
-define("APP_VERSION", "1.19.23.5324");
+define("APP_VERSION", "1.19.23.5325");
 define("APP_DIR", dirname(__FILE__));
 define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib");
 define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates");
 // Date format for database and URI parameters.
 define('DB_DATEFORMAT', '%Y-%m-%d');
diff --git a/tofile.php b/tofile.php
index 8b3ae230..996ffe9f 100644
--- a/tofile.php
+++ b/tofile.php
@@ -216,22 +216,22 @@ if ('csv' == $type) {
     // User custom field labels.
     if ($custom_fields && $custom_fields->userFields) {
       foreach ($custom_fields->userFields as $userField) {
         $field_name = 'user_field_'.$userField['id'];
         $checkbox_control_name = 'show_'.$field_name;
-        if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$userField['label']).'"';
+        if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($userField['label']).'"';
       }
     }
     if ($bean->getAttribute('chclient')) print ',"'.$i18n->get('label.client').'"';
     if ($bean->getAttribute('chproject')) print ',"'.$i18n->get('label.project').'"';
     if ($bean->getAttribute('chtask')) print ',"'.$i18n->get('label.task').'"';
     // Time custom field labels.
     if ($custom_fields && $custom_fields->timeFields) {
       foreach ($custom_fields->timeFields as $timeField) {
         $field_name = 'time_field_'.$timeField['id'];
         $checkbox_control_name = 'show_'.$field_name;
-        if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$timeField['label']).'"';
+        if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($timeField['label']).'"';
       }
     }
     if ($bean->getAttribute('chstart')) print ',"'.$i18n->get('label.start').'"';
     if ($bean->getAttribute('chfinish')) print ',"'.$i18n->get('label.finish').'"';
     if ($bean->getAttribute('chduration')) print ',"'.$i18n->get('label.duration').'"';
@@ -246,28 +246,28 @@ if ('csv' == $type) {
     print "\n";
 
     // Print items.
     foreach ($items as $item) {
       print '"'.$item['date'].'"';
-      if ($user->can('view_reports') || $user->can('view_all_reports') || $user->isClient()) print ',"'.str_replace('"','""',$item['user']).'"';
+      if ($user->can('view_reports') || $user->can('view_all_reports') || $user->isClient()) print ',"'.ttNeutralizeForCsv($item['user']).'"';
       // User custom fields.
       if ($custom_fields && $custom_fields->userFields) {
         foreach ($custom_fields->userFields as $userField) {
           $field_name = 'user_field_'.$userField['id'];
           $checkbox_control_name = 'show_'.$field_name;
-          if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$item[$field_name]).'"';
+          if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($item[$field_name]).'"';
         }
       }
-      if ($bean->getAttribute('chclient')) print ',"'.str_replace('"','""',$item['client']).'"';
-      if ($bean->getAttribute('chproject')) print ',"'.str_replace('"','""',$item['project']).'"';
-      if ($bean->getAttribute('chtask')) print ',"'.str_replace('"','""',$item['task']).'"';
+      if ($bean->getAttribute('chclient')) print ',"'.ttNeutralizeForCsv($item['client']).'"';
+      if ($bean->getAttribute('chproject')) print ',"'.ttNeutralizeForCsv($item['project']).'"';
+      if ($bean->getAttribute('chtask')) print ',"'.ttNeutralizeForCsv($item['task']).'"';
       // Time custom fields.
       if ($custom_fields && $custom_fields->timeFields) {
         foreach ($custom_fields->timeFields as $timeField) {
           $field_name = 'time_field_'.$timeField['id'];
           $checkbox_control_name = 'show_'.$field_name;
-          if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$item[$field_name]).'"';
+          if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($item[$field_name]).'"';
         }
       }
       if ($bean->getAttribute('chstart')) print ',"'.$item['start'].'"';
       if ($bean->getAttribute('chfinish')) print ',"'.$item['finish'].'"';
       if ($bean->getAttribute('chduration')) {
@@ -275,11 +275,11 @@ if ('csv' == $type) {
         if($val && isTrue('EXPORT_DECIMAL_DURATION'))
           $val = time_to_decimal($val);
         print ',"'.$val.'"';
       }
       if ($bean->getAttribute('chunits')) print ',"'.$item['units'].'"';
-      if ($bean->getAttribute('chnote')) print ',"'.str_replace('"','""',$item['note']).'"';
+      if ($bean->getAttribute('chnote')) print ',"'.ttNeutralizeForCsv($item['note']).'"';
       if ($bean->getAttribute('chcost')) {
         if ($user->can('manage_invoices') || $user->isClient())
           print ',"'.$item['cost'].'"';
         else
           print ',"'.$item['expense'].'"';
@@ -288,11 +288,11 @@ if ('csv' == $type) {
       if ($bean->getAttribute('chpaid')) print ',"'.$item['paid'].'"';
       if ($bean->getAttribute('chip')) {
         $ip = $item['modified'] ? $item['modified_ip'].' '.$item['modified'] : $item['created_ip'].' '.$item['created'];
         print ',"'.$ip.'"';
       }
-      if ($bean->getAttribute('chinvoice')) print ',"'.str_replace('"','""',$item['invoice']).'"';
-      if ($bean->getAttribute('chtimesheet')) print ',"'.str_replace('"','""',$item['timesheet_name']).'"';
+      if ($bean->getAttribute('chinvoice')) print ',"'.ttNeutralizeForCsv($item['invoice']).'"';
+      if ($bean->getAttribute('chtimesheet')) print ',"'.ttNeutralizeForCsv($item['timesheet_name']).'"';
       print "\n";
     }
   }
 }

Vulnerable code: define("APP_VERSION", "1.19.23.5324");
        if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$userField['label']).'"';
        if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$timeField['label']).'"';
      if ($user->can('view_reports') || $user->can('view_all_reports') || $user->isClient()) print ',"'.str_replace('"','""',$item['user']).'"';
          if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$item[$field_name]).'"';
      if ($bean->getAttribute('chclient')) print ',"'.str_replace('"','""',$item['client']).'"';
      if ($bean->getAttribute('chproject')) print ',"'.str_replace('"','""',$item['project']).'"';
      if ($bean->getAttribute('chtask')) print ',"'.str_replace('"','""',$item['task']).'"';
          if ($bean->getAttribute($checkbox_control_name)) print ',"'.str_replace('"','""',$item[$field_name]).'"';
      if ($bean->getAttribute('chnote')) print ',"'.str_replace('"','""',$item['note']).'"';
      if ($bean->getAttribute('chinvoice')) print ',"'.str_replace('"','""',$item['invoice']).'"';
      if ($bean->getAttribute('chtimesheet')) print ',"'.str_replace('"','""',$item['timesheet_name']).'"';
Fixed code: 
// ttNeutralizeForCsv neutralizes user input for export to CSV files
// by removing =, +, -, and @ characters from the beginning of cell values.
// This mitigates a risk of CSV injection, see https://owasp.org/www-community/attacks/CSV_Injection
// Additionally, it replaces each quote character with a double quote.
function ttNeutralizeForCsv($val) {
  $result = ltrim($val, '=+-@');
  return str_replace('"', '""', $result);
}
define("APP_VERSION", "1.19.23.5325");
        if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($userField['label']).'"';
        if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($timeField['label']).'"';
      if ($user->can('view_reports') || $user->can('view_all_reports') || $user->isClient()) print ',"'.ttNeutralizeForCsv($item['user']).'"';
          if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($item[$field_name]).'"';
      if ($bean->getAttribute('chclient')) print ',"'.ttNeutralizeForCsv($item['client']).'"';
      if ($bean->getAttribute('chproject')) print ',"'.ttNeutralizeForCsv($item['project']).'"';
      if ($bean->getAttribute('chtask')) print ',"'.ttNeutralizeForCsv($item['task']).'"';
          if ($bean->getAttribute($checkbox_control_name)) print ',"'.ttNeutralizeForCsv($item[$field_name]).'"';
      if ($bean->getAttribute('chnote')) print ',"'.ttNeutralizeForCsv($item['note']).'"';
      if ($bean->getAttribute('chinvoice')) print ',"'.ttNeutralizeForCsv($item['invoice']).'"';
      if ($bean->getAttribute('chtimesheet')) print ',"'.ttNeutralizeForCsv($item['timesheet_name']).'"';