Let users download a CSV of inbound messages

In user research, we’ve seen users copy/pasting the contents of the
inbound SMS page into a spreadsheet, in order to keep a record of the
messages they receive. They even went as far as to write a macro which
fixed the errors caused by copying and pasting.

It would be much easier if we just gave them the data already in a
spreadsheet format. Which is what this commit does.

One caveat is that, because spreadsheets can contain executable code (ie
formulas), and because we’re populating the spreadsheet with
user-submitted data (albeit via SMS) we need to be careful about
injection attacks.

The details of how these attacks work are detailed here (interesting
reading): http://georgemauer.net/2017/10/07/csv-injection.html

The mitigation is to not allow characters which initialise a formula
at the start of the cell.
This commit is contained in:
Chris Hill-Scott
2017-10-17 11:41:12 +01:00
parent ad8a35b045
commit c9b2211bd3
5 changed files with 126 additions and 2 deletions

View File

@@ -6,7 +6,8 @@ from flask import (
session,
jsonify,
request,
abort
abort,
Response,
)
from flask_login import login_required
@@ -20,6 +21,9 @@ from app import (
service_api_client,
template_statistics_client,
inbound_number_client,
format_datetime_short,
format_date_numeric,
format_datetime_numeric,
)
from app.statistics_utils import get_formatted_percentage, add_rate_to_job
from app.utils import (
@@ -27,6 +31,7 @@ from app.utils import (
get_current_financial_year,
FAILURE_STATUSES,
REQUESTED_STATUSES,
Spreadsheet,
)
@@ -161,6 +166,31 @@ def inbox_updates(service_id):
return jsonify(get_inbox_partials(service_id))
@main.route("/services/<service_id>/inbox.csv")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def inbox_download(service_id):
return Response(
Spreadsheet.from_rows(
[[
'Phone number',
'Message',
'Received',
]] + [[
message['user_number'],
message['content'].lstrip(('=+-@')),
format_datetime_numeric(message['created_at']),
] for message in service_api_client.get_inbound_sms(service_id)]
).as_csv_data,
mimetype='text/csv',
headers={
'Content-Disposition': 'inline; filename="Received text messages {}.csv"'.format(
format_date_numeric(datetime.utcnow().isoformat())
)
}
)
def get_inbox_partials(service_id):
if 'inbound_sms' not in current_service['permissions']: