fix(csv): neutralizza injection di formule nei campi testuali export (#22)
lot_number, serial_number (export manuale e di produzione), station.code, recipe.code, subtask.description finivano nel CSV cosi' come arrivavano: un lotto '=HYPERLINK(...)' apriva un link vivo in Excel/LibreOffice. Nuova neutralise_csv_cell() in production_export_service.py, importata da measurements.py: se il campo (spazi iniziali esclusi) comincia con =, +, -, @, TAB o CR antepone un apice, cosi' resta testo letterale. Applicata solo ai campi testuali, mai ai numeri. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,26 @@ EXPORT_SUBDIR = "statistics"
|
||||
_VALID_CSV_DELIMITERS = {",", ";", "\t", "|"}
|
||||
_FALLBACK_CSV_DELIMITER = ";"
|
||||
|
||||
# A cell starting with one of these (after leading spaces) is read as a formula
|
||||
# by Excel/LibreOffice when the CSV is opened — CSV/formula injection (OWASP).
|
||||
_DANGEROUS_CSV_PREFIXES = ("=", "+", "-", "@", "\t", "\r")
|
||||
|
||||
|
||||
def neutralise_csv_cell(value: str) -> str:
|
||||
"""Defuse a formula-injection payload in a text CSV cell.
|
||||
|
||||
Prepends a single quote when the value, stripped of leading spaces, starts
|
||||
with =, +, -, @, TAB or CR: this keeps spreadsheet software from evaluating
|
||||
it as a formula while the value stays valid, literal CSV text. Only for
|
||||
free-text fields (lot, serial, descriptions, names, codes) — never call
|
||||
this on numeric cells, the quote would make them text.
|
||||
"""
|
||||
stripped = value.lstrip(" ")
|
||||
if stripped and stripped[0] in _DANGEROUS_CSV_PREFIXES:
|
||||
return "'" + value
|
||||
return value
|
||||
|
||||
|
||||
HEADER = [
|
||||
"production_run_id",
|
||||
"station_code",
|
||||
@@ -127,15 +147,15 @@ async def build_statistics_csv(db: AsyncSession, run: ProductionRun) -> str:
|
||||
for measurement, subtask in rows:
|
||||
writer.writerow([
|
||||
run.id,
|
||||
station.code if station else "",
|
||||
recipe.code if recipe else "",
|
||||
neutralise_csv_cell(station.code) if station else "",
|
||||
neutralise_csv_cell(recipe.code) if recipe else "",
|
||||
measurement.version_id,
|
||||
measurement.lot_number or "",
|
||||
measurement.serial_number or "",
|
||||
neutralise_csv_cell(measurement.lot_number or ""),
|
||||
neutralise_csv_cell(measurement.serial_number or ""),
|
||||
measurement.id,
|
||||
measurement.subtask_id,
|
||||
subtask.marker_number if subtask else "",
|
||||
subtask.description if subtask else "",
|
||||
neutralise_csv_cell(subtask.description) if subtask else "",
|
||||
number(subtask.nominal) if subtask else "",
|
||||
number(measurement.value),
|
||||
number(measurement.deviation),
|
||||
|
||||
Reference in New Issue
Block a user