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"