Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Glandos committed May 14, 2022
1 parent 8b93700 commit 042b33a
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 1 deletion.
32 changes: 32 additions & 0 deletions ihatemoney/tests/import_test.py
Expand Up @@ -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):
Expand Down
16 changes: 15 additions & 1 deletion ihatemoney/utils.py
Expand Up @@ -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
Expand All @@ -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)
Expand Down

0 comments on commit 042b33a

Please sign in to comment.