CVE-2021-25962: Improper Neutralization of Formula Elements in a CSV File (CWE-1236)
value: “Shuup” application in versions 0.4.2 to 2.10.8 is affected by the “Formula Injection” vulnerability. A customer can inject payloads in the name input field in the billing address while buying a product. When a store administrator accesses the reports page to export the data as an Excel file and opens it, the payload gets executed.
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: 8.8
Published: 2021-09-29T14:15Z
Repository: https://github.com/shuup/shuup
Language: Python
Files changed: CHANGELOG.md, shuup/reports/writer.py
Commit date: 2021-07-07 14:27:26 -0300
Commit message: Reports: clean malicius content from the HTML and CSV exporters

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 143064aeb..05c96cb04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,10 +12,11 @@ List all changes after the last release here (newer on top). Each change on a se
 
 - General: fix critical vulnerability on views that were returning not escaped content making it open to XSS attacks
 
 ### Changed
 
+- Reports: clean malicius content from the HTML and CSV exporters
 - Reports: prevent formulas from being exported in excel writer
 - Tests: log errors into a log file
 - Admin: hide email template button based on permission
 - Reports: improve log when an importer fails
 
diff --git a/shuup/reports/writer.py b/shuup/reports/writer.py
index 7f2cca4ce..fa9bf7c74 100644
--- a/shuup/reports/writer.py
+++ b/shuup/reports/writer.py
@@ -3,10 +3,11 @@
 #
 # Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
 #
 # This source code is licensed under the OSL-3.0 license found in the
 # LICENSE file in the root directory of this source tree.
+import bleach
 import csv
 import six
 from babel.dates import format_datetime
 from datetime import datetime
 from decimal import Decimal
@@ -152,10 +153,17 @@ def format_data(data, format_iso_dates=False, format_money_values=False):
         return get_locally_formatted_datetime(data)
 
     return data
 
 
+def remove_unsafe_chars(data):
+    if isinstance(data, str):
+        return "".join([char for char in data if char not in ("=", "+", "-")])
+
+    return data
+
+
 class CSVReportWriter(ReportWriter):
     content_type = "text/csv"
     extension = ".csv"
     writer_type = "csv"
 
@@ -168,16 +176,16 @@ class CSVReportWriter(ReportWriter):
 
     def write_data_table(self, report, report_data, has_totals=True):
         self.data.append([c["title"] for c in report.schema])
         for datum in report_data:
             datum = report.read_datum(datum)
-            self.data.append([format_data(data, format_iso_dates=True) for data in datum])
+            self.data.append([format_data(remove_unsafe_chars(data), format_iso_dates=True) for data in datum])
 
         if has_totals:
             for datum in report.get_totals(report_data):
                 datum = report.read_datum(datum)
-                self.data.append([format_data(data) for data in datum])
+                self.data.append([format_data(remove_unsafe_chars(data)) for data in datum])
 
     def get_rendered_output(self):
         f = StringIO()
         csv.writer(f).writerows(self.data)
         return f.getvalue().encode()
@@ -204,17 +212,17 @@ class ExcelReportWriter(ReportWriter):
 
     def write_data_table(self, report, report_data, has_totals=True):
         self.worksheet.append([c["title"] for c in report.schema])
         for datum in report_data:
             datum = report.read_datum(datum)
-            self.worksheet.append([format_data(data) for data in datum])
+            self.worksheet.append([format_data(remove_unsafe_chars(data)) for data in datum])
             self._convert_row_to_string()
 
         if has_totals:
             for datum in report.get_totals(report_data):
                 datum = report.read_datum(datum)
-                self.worksheet.append([format_data(data) for data in datum])
+                self.worksheet.append([format_data(remove_unsafe_chars(data)) for data in datum])
                 self._convert_row_to_string()
 
     def write_page_heading(self, text):
         self.worksheet.append([text])
         self.worksheet.append([""])
@@ -262,11 +270,11 @@ class HTMLReportWriter(ReportWriter):
 
     def _w_raw(self, content):
         self.output.append(mark_safe(content))
 
     def _w(self, content):
-        self.output.append(format_data(content, format_money_values=True))
+        self.output.append(bleach.clean(str(format_data(content, format_money_values=True)), strip=True))
 
     def _w_tag(self, tag, content):
         self._w_raw("<%s>" % tag)
         self._w(content)
         self._w_raw("</%s>" % tag)

Vulnerable code:             self.data.append([format_data(data, format_iso_dates=True) for data in datum])
                self.data.append([format_data(data) for data in datum])
            self.worksheet.append([format_data(data) for data in datum])
                self.worksheet.append([format_data(data) for data in datum])
        self.output.append(format_data(content, format_money_values=True))
Fixed code: - Reports: clean malicius content from the HTML and CSV exporters
import bleach
def remove_unsafe_chars(data):
    if isinstance(data, str):
        return "".join([char for char in data if char not in ("=", "+", "-")])

    return data


            self.data.append([format_data(remove_unsafe_chars(data), format_iso_dates=True) for data in datum])
                self.data.append([format_data(remove_unsafe_chars(data)) for data in datum])
            self.worksheet.append([format_data(remove_unsafe_chars(data)) for data in datum])
                self.worksheet.append([format_data(remove_unsafe_chars(data)) for data in datum])
        self.output.append(bleach.clean(str(format_data(content, format_money_values=True)), strip=True))