Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion fleet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import random
import requests
import csv
from datetime import date, datetime, time, timedelta
from itertools import groupby, chain
from functools import cmp_to_key
Expand Down Expand Up @@ -1119,6 +1120,55 @@ def vehicles_api(request, operator_slug):
'total_count': total_count,
})

def export_fleet_csv(request, operator_slug):
operator = get_object_or_404(MBTOperator, operator_slug=operator_slug)

withdrawn = request.GET.get('withdrawn', '').lower() == 'true'
depot = request.GET.get('depot')

# Base queryset
qs = fleet.objects.filter(Q(operator=operator) | Q(loan_operator=operator))
if not withdrawn:
qs = qs.filter(in_service=True)
if depot:
qs = qs.filter(depot=depot)

# Order by fleet number
vehicles = qs.order_by('fleet_number_sort').select_related(
'operator', 'loan_operator', 'vehicleType', 'livery'
)

# Create the HttpResponse object with CSV header
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{operator_slug}_fleet.csv"'

writer = csv.writer(response)

# Write header row
headers = ['Fleet Number', 'Registration', 'Type', 'Type Details', 'Livery',
'Branding', 'Depot', 'Name', 'Previous Reg', 'Features', 'In Service',
'Open Top', 'Preserved']
writer.writerow(headers)

# Write data rows
for v in vehicles:
writer.writerow([
v.fleet_number or '',
v.reg or '',
v.vehicleType.type_name if v.vehicleType else '',
v.type_details or '',
v.livery.name if v.livery else '',
v.branding or '',
v.depot or '',
v.name or '',
v.prev_reg or '',
', '.join(v.features) if v.features else '',
'Yes' if v.in_service else 'No',
'Yes' if v.open_top else 'No',
'Yes' if v.preserved else 'No',
])

return response
Comment on lines +1123 to +1171
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Overall CSV export logic looks good; consider hardening against CSV injection and legacy features types

The view mirrors vehicles_api’s filtering and ordering, and the selected columns/boolean formatting are all consistent with the data model, so functionally this is solid.

A couple of points worth tightening:

  • User‑controlled text (e.g. fleet_number, reg, name, branding, depot, prev_reg, features) is written directly to CSV. If an attacker can put a value starting with =, +, -, @ or a leading tab into any of those fields, opening the CSV in Excel/Sheets can trigger formula execution (“CSV injection”). Consider a small sanitizer applied to each string field before writing, for example:

    def safe_csv_cell(value):
        if not isinstance(value, str):
            return value
        if value and value[0] in ("=", "+", "-", "@", "\t"):
            return "'" + value
        return value

    and wrap the text fields (fleet_number, reg, type_details, livery.name, branding, depot, name, prev_reg, features) with safe_csv_cell(...) before passing them to writer.writerow. This keeps the export usable while mitigating formula injection in common spreadsheet tools.

  • features is a JSONField; most of the code paths now store a list of strings, but older rows might still contain a bare string or some other shape. ', '.join(v.features) will misbehave (or even raise) in those cases. A defensive pattern like:

    if isinstance(v.features, list):
        features_str = ", ".join(map(str, v.features))
    elif isinstance(v.features, str):
        features_str = v.features
    else:
        features_str = ""

    would make the export more robust against legacy data.

These are incremental hardening steps; the core view implementation is otherwise in good shape.

🧰 Tools
🪛 Ruff (0.14.10)

1124-1124: MBTOperator may be undefined, or defined from star imports

(F405)


1130-1130: fleet may be undefined, or defined from star imports

(F405)


1130-1130: Q may be undefined, or defined from star imports

(F405)


1130-1130: Q may be undefined, or defined from star imports

(F405)

🤖 Prompt for AI Agents
In @fleet/views.py around lines 1123 - 1171, export_fleet_csv writes
user-controlled text directly to CSV and assumes v.features is always a list;
add a small sanitizer function (e.g., safe_csv_cell) and apply it to all string
fields written (fleet_number, reg, type_details, livery.name, branding, depot,
name, prev_reg, and the features cell) to prefix a quote when values start with
"=", "+", "-", "@", or a tab, and make the features cell robust by normalizing
v.features before joining (handle list, str, and other types safely and convert
elements to strings). Locate export_fleet_csv and wrap each text value passed
into writer.writerow with the sanitizer and replace the direct ',
'.join(v.features) usage with a defensive normalization that yields a single
string for the features column.


def vehicle_detail(request, operator_slug, vehicle_id):
response = feature_enabled(request, "view_vehicles")
Expand Down Expand Up @@ -6783,4 +6833,4 @@ def route_update_delete(request, operator_slug, route_id, update_id):
'update': update,
'route_id': route_id,
'operator_slug': operator_slug
})
})