fix(sf-fe-a5): address review #346 — CSV injection + area_m2 NaN fallback + nits
🟠 HIGH (security): ExportButtons.escapeCsvCell now prefixes cells starting with =/+/-/@/tab/CR with a leading apostrophe (OWASP CWE-1236). Prevents Excel/Calc/Sheets from evaluating attacker-controllable strings (egrn.address, cad_num) as formulas (=cmd|... / =HYPERLINK / DDE). 🟡 MEDIUM: buildFallbackEgrn now returns area_m2: NaN instead of 0; KPI 'Площадь' and EgrnPropertyTable 'Площадь' row both gate on Number.isFinite + >0, rendering '—' consistently when EGRN absent (previously asymmetric: KPI '—' / table '0 м²'). 🟢 LOW: scoreVerdict collapsed dead branch (both >=80 and >=65 returned same ПОДХОДИТ label). MiniMap toSiteMapData narrows source via explicit ternary instead of unchecked 'as' cast.
This commit is contained in:
parent
6fe8d637fe
commit
47d0374386
4 changed files with 17 additions and 9 deletions
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue