From 042b33aeb2e87ad652e1e0478ce1b2b6bfcb9210 Mon Sep 17 00:00:00 2001 From: Glandos Date: Sat, 14 May 2022 15:53:15 +0200 Subject: [PATCH] escape csv formulae This is only needed for unsecure spreadsheet applications (hi Google Docs and MS Excel) that load formulae by default. See https://owasp.org/www-community/attacks/CSV_Injection for some mitigation explanation. This is not complete, but it should be OK for now. --- ihatemoney/tests/import_test.py | 32 ++++++++++++++++++++++++++++++++ ihatemoney/utils.py | 16 +++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/ihatemoney/tests/import_test.py b/ihatemoney/tests/import_test.py index f1ee1d07a..bccb254d6 100644 --- a/ihatemoney/tests/import_test.py +++ b/ihatemoney/tests/import_test.py @@ -596,6 +596,38 @@ def test_export_with_currencies(self): set(line.split(",")), set(received_lines[i].strip("\r").split(",")) ) + def test_export_escape_formulae(self): + self.post_project("raclette", default_currency="EUR") + + # add participants + self.client.post("/raclette/members/add", data={"name": "zorglub"}) + + # create bills + self.client.post( + "/raclette/add", + data={ + "date": "2016-12-31", + "what": "=COS(36)", + "payer": 1, + "payed_for": [1], + "amount": "10.0", + "original_currency": "EUR", + }, + ) + + # generate csv export of bills + resp = self.client.get("/raclette/export/bills.csv") + expected = [ + "date,what,amount,currency,payer_name,payer_weight,owers", + "2016-12-31,'=COS(36),10.0,EUR,zorglub,1.0,zorglub", + ] + received_lines = resp.data.decode("utf-8").split("\n") + + for i, line in enumerate(expected): + self.assertEqual( + set(line.split(",")), set(received_lines[i].strip("\r").split(",")) + ) + class ImportTestCaseJSON(CommonTestCase.Import): def generate_form_data(self, data): diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py index ddfcf3569..513d35eac 100644 --- a/ihatemoney/utils.py +++ b/ihatemoney/utils.py @@ -152,6 +152,17 @@ def list_of_dicts2json(dict_to_convert): return BytesIO(dumps(dict_to_convert).encode("utf-8")) +def escape_csv_formulae(value): + # See https://owasp.org/www-community/attacks/CSV_Injection + if ( + value + and isinstance(value, str) + and value[0] in ["=", "+", "-", "@", "\t", "\n"] + ): + return f"'{value}" + return value + + def list_of_dicts2csv(dict_to_convert): """Take a list of dictionnaries and turns it into a csv in-memory file, assume all dict have the same keys @@ -164,7 +175,10 @@ def list_of_dicts2csv(dict_to_convert): # (expecting a sequence getting a view) csv_data = [list(dict_to_convert[0].keys())] for dic in dict_to_convert: - csv_data.append([dic[h] for h in dict_to_convert[0].keys()]) + csv_data.append( + [escape_csv_formulae(dic[h]) for h in dict_to_convert[0].keys()] + ) + # csv_data.append([dic[h] for h in dict_to_convert[0].keys()]) except (KeyError, IndexError): csv_data = [] writer = csv.writer(csv_file)