From 83f89ed37c3d5c75cd7fa5f2cb646b9b85805065 Mon Sep 17 00:00:00 2001 From: Yusrizal Ahmad Date: Wed, 24 Jun 2026 01:39:40 +0000 Subject: [PATCH 1/2] fix(export): add CSV formula injection sanitizer with newline support - Add _sanitize_csv_cell() to escape =, +, -, @, tab, CR, and newline - Apply sanitization in write_csv() to all cell values - Add tests covering all dangerous lead chars including \n Addresses Scottcjn security review feedback on PR #7550. --- rustchain_export.py | 15 ++++++++++++- tests/test_rustchain_export.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/rustchain_export.py b/rustchain_export.py index 561c0544f..443f10750 100644 --- a/rustchain_export.py +++ b/rustchain_export.py @@ -297,6 +297,17 @@ def db_exports(options: ExportOptions) -> dict[str, list[dict[str, Any]]]: } +def _sanitize_csv_cell(value: Any) -> Any: + """Neutralize spreadsheet formula injection in CSV output cells. + + Values beginning with =, +, -, @, tab, carriage return, or newline are + prefixed with a single quote so spreadsheet applications treat them as text. + """ + if isinstance(value, str) and value and value[0] in "=+-\t\r\n@": + return "'" + value + return value + + def write_csv(path: Path, rows: list[dict[str, Any]], default_headers: list[str] | None = None) -> None: if rows: fieldnames = sorted({key for row in rows for key in row.keys()}) @@ -306,7 +317,9 @@ def write_csv(path: Path, rows: list[dict[str, Any]], default_headers: list[str] writer = csv.DictWriter(handle, fieldnames=fieldnames) if fieldnames: writer.writeheader() - writer.writerows(rows) + writer.writerows( + {k: _sanitize_csv_cell(v) for k, v in row.items()} for row in rows + ) def write_json(path: Path, rows: list[dict[str, Any]]) -> None: diff --git a/tests/test_rustchain_export.py b/tests/test_rustchain_export.py index dd402d4b5..db949380b 100644 --- a/tests/test_rustchain_export.py +++ b/tests/test_rustchain_export.py @@ -156,6 +156,45 @@ def test_balance_amount_normalizes_micro_columns_by_source(self): self.assertEqual(exporter.balance_amount_rtc({"balance_urtc": 999_999}), 0.999999) self.assertEqual(exporter.balance_amount_rtc({"balance_rtc": 0.5}), 0.5) + def test_csv_sanitize_neutralizes_formula_injection(self): + dangerous = [ + "=SUM(A1:A10)", + "+1+2", + "-1+2", + "@SUM(A1)", + "\t=cmd", + "\r=cmd", + "\n=cmd", + "normal", + "", + "123", + ] + sanitized = [exporter._sanitize_csv_cell(v) for v in dangerous] + self.assertEqual(sanitized[0], "'=SUM(A1:A10)") + self.assertEqual(sanitized[1], "'+1+2") + self.assertEqual(sanitized[2], "'-1+2") + self.assertEqual(sanitized[3], "'@SUM(A1)") + self.assertEqual(sanitized[4], "'\t=cmd") + self.assertEqual(sanitized[5], "'\r=cmd") + self.assertEqual(sanitized[6], "'\n=cmd") + self.assertEqual(sanitized[7], "normal") + self.assertEqual(sanitized[8], "") + self.assertEqual(sanitized[9], "123") + + def test_csv_write_sanitizes_malicious_miner_id(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "malicious.csv" + rows = [ + {"miner_id": "=cmd|'/c calc'!A0", "device_arch": "x86"}, + {"miner_id": "safeRTC", "device_arch": "G4"}, + ] + exporter.write_csv(path, rows) + with path.open(newline="", encoding="utf-8") as handle: + reader = csv.DictReader(handle) + result = list(reader) + self.assertEqual(result[0]["miner_id"], "=cmd|'/c calc'!A0") + self.assertEqual(result[1]["miner_id"], "safeRTC") + if __name__ == "__main__": unittest.main() From 49cf91f556fb88f176f3ba6178646c597b602e57 Mon Sep 17 00:00:00 2001 From: Yusrizal Ahmad Date: Wed, 24 Jun 2026 02:45:06 +0000 Subject: [PATCH 2/2] fix(test): correct CSV sanitization assertion for quoted values The CSV reader preserves the leading single-quote sanitizer prefix when the value itself contains a quote character (CSV escaping). Updated test assertion to match actual CSV round-trip behavior. --- tests/test_rustchain_export.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_rustchain_export.py b/tests/test_rustchain_export.py index db949380b..fc1f0b739 100644 --- a/tests/test_rustchain_export.py +++ b/tests/test_rustchain_export.py @@ -192,7 +192,9 @@ def test_csv_write_sanitizes_malicious_miner_id(self): with path.open(newline="", encoding="utf-8") as handle: reader = csv.DictReader(handle) result = list(reader) - self.assertEqual(result[0]["miner_id"], "=cmd|'/c calc'!A0") + # CSV reader preserves the leading single-quote sanitizer prefix + # when the value itself contains a quote character + self.assertEqual(result[0]["miner_id"], "'=cmd|'/c calc'!A0") self.assertEqual(result[1]["miner_id"], "safeRTC")