From 3589f53c3c4ab8c68cb087c48a30d269f2610110 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 7 Nov 2026 02:46:25 +0400 Subject: [PATCH] =?UTF-8?q?fix(#190):=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=83=D0=BB=D1=8B=20=D0=B2=20CSV-=D0=B2=D1=8B=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=B7=D0=BA=D0=B5=20=D0=BA=D0=B0=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20(T15.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 --- api/src/services/quality_metrics_service.py | 5 +++++ api/tests/test_api_quality_metrics.py | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/api/src/services/quality_metrics_service.py b/api/src/services/quality_metrics_service.py index c6e4926..34a3db9 100644 --- a/api/src/services/quality_metrics_service.py +++ b/api/src/services/quality_metrics_service.py @@ -354,6 +354,9 @@ class QualityMetricsService: return buffer.getvalue().encode("utf-8-sig"), "text/csv; charset=utf-8", "csv" +_FORMULA_PREFIXES = ("=", "+", "-", "@", "\t", "\r") + + def _cell(value: Any) -> Any: """Значение ячейки CSV: перечисления — их значение, даты — ISO, булевы — true/false.""" if isinstance(value, bool): @@ -362,4 +365,6 @@ def _cell(value: Any) -> Any: return value.value if isinstance(value, datetime | date): return value.isoformat() + if isinstance(value, str) and value.startswith(_FORMULA_PREFIXES): + return "'" + value # защита от CSV-инъекции формул в Excel return value diff --git a/api/tests/test_api_quality_metrics.py b/api/tests/test_api_quality_metrics.py index e37547d..7d3f3a2 100644 --- a/api/tests/test_api_quality_metrics.py +++ b/api/tests/test_api_quality_metrics.py @@ -30,7 +30,7 @@ from src.models.enums import ( ) from src.repositories.quality_facts import insert_failure_fact, insert_intake_fact, insert_outcome_fact from src.repositories.users import UserWithAccess -from src.services.quality_metrics_service import EXPORT_COLUMNS +from src.services.quality_metrics_service import EXPORT_COLUMNS, _cell from tests.conftest import AuthClientFactory HTTP_OK, HTTP_FORBIDDEN, HTTP_UNPROCESSABLE = 200, 403, 422 @@ -322,3 +322,15 @@ async def test_quality_metrics_export(auth_client: AuthClientFactory, seeded: As assert rows[0]["is_scan"] == "true" as_json = await client.get(f"{BASE}/export", params={"format": "json", "date_from": "2026-11-01"}) assert len(as_json.json()) == 2 # noqa: PLR2004 + + +@pytest.mark.parametrize("prefix", ["=", "+", "-", "@", "\t", "\r"]) +def test_csv_cell_escapes_formula_strings(prefix: str) -> None: + assert _cell(f"{prefix}SUM(A1)") == f"'{prefix}SUM(A1)" + + +def test_csv_cell_keeps_non_strings_and_plain_strings() -> None: + assert _cell(-5) == -5 # noqa: PLR2004 + assert _cell("ООО Альфа") == "ООО Альфа" + assert _cell(datetime.date(2026, 11, 1)) == "2026-11-01" + assert _cell(True) == "true"