diff --git a/frontend/src/components/site-finder/analysis/EgrnPropertyTable.tsx b/frontend/src/components/site-finder/analysis/EgrnPropertyTable.tsx index 9a6d5018..b0bed5cd 100644 --- a/frontend/src/components/site-finder/analysis/EgrnPropertyTable.tsx +++ b/frontend/src/components/site-finder/analysis/EgrnPropertyTable.tsx @@ -31,7 +31,10 @@ function buildRows(data: ParcelEgrn): Row[] { { label: "Адрес", value: data.address }, { label: "Площадь", - value: `${data.area_m2.toLocaleString("ru")} м²`, + value: + Number.isFinite(data.area_m2) && data.area_m2 > 0 + ? `${data.area_m2.toLocaleString("ru")} м²` + : "—", mono: true, }, { label: "ВРИ", value: data.vri }, diff --git a/frontend/src/components/site-finder/analysis/ExportButtons.tsx b/frontend/src/components/site-finder/analysis/ExportButtons.tsx index 28adea01..b0aadf7f 100644 --- a/frontend/src/components/site-finder/analysis/ExportButtons.tsx +++ b/frontend/src/components/site-finder/analysis/ExportButtons.tsx @@ -55,12 +55,18 @@ function buildCsvRows(data: ParcelAnalyzeResponse): string[][] { } function escapeCsvCell(cell: string): string { - // Wrap in quotes if contains comma, quote, or newline - const needsQuotes = /[",\n\r]/.test(cell); - if (needsQuotes) { - return `"${cell.replace(/"/g, '""')}"`; + // CSV-injection mitigation (OWASP / CWE-1236): cells starting with =, +, -, @, + // tab, or CR are evaluated as formulas by Excel/Calc/Sheets. Prefix dangerous + // starters with a leading apostrophe so the spreadsheet treats them as text. + let safe = cell; + if (/^[=+\-@\t\r]/.test(safe)) { + safe = "'" + safe; } - return cell; + const needsQuotes = /[",\n\r]/.test(safe); + if (needsQuotes) { + return `"${safe.replace(/"/g, '""')}"`; + } + return safe; } function generateCsvBlob(rows: string[][]): Blob { diff --git a/frontend/src/components/site-finder/analysis/MiniMap.tsx b/frontend/src/components/site-finder/analysis/MiniMap.tsx index 8e9e185e..93738953 100644 --- a/frontend/src/components/site-finder/analysis/MiniMap.tsx +++ b/frontend/src/components/site-finder/analysis/MiniMap.tsx @@ -51,7 +51,7 @@ interface Props { function toSiteMapData(data: ParcelAnalyzeResponse): ParcelAnalysis { return { cad_num: data.cad_num, - source: (data.source as ParcelAnalysis["source"]) ?? "cad_quarter", + source: data.source === "cad_building" ? "cad_building" : "cad_quarter", geom_geojson: (data.geom_geojson as Geometry) ?? null, score_breakdown: data.score_breakdown, score: data.score, diff --git a/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx b/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx index 7f268f8c..82b482d2 100644 --- a/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx +++ b/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx @@ -31,7 +31,7 @@ function buildFallbackEgrn(cad: string): ParcelEgrn { return { cad_num: cad, address: "—", - area_m2: 0, + area_m2: NaN, vri: "—", category: "—", registration_date: null, @@ -57,7 +57,6 @@ function scoreVerdict(score: number, scoreLabel: string | undefined): string { const labelUpper = label.toUpperCase(); - if (score >= 80) return `ПОДХОДИТ · ${labelUpper}`; if (score >= 65) return `ПОДХОДИТ · ${labelUpper}`; if (score >= 45) return `СРЕДНЕ · ${labelUpper}`; return `РИСКИ · ${labelUpper}`;