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.address },
|
||||||
{
|
{
|
||||||
label: "Площадь",
|
label: "Площадь",
|
||||||
value: `${data.area_m2.toLocaleString("ru")} м²`,
|
value:
|
||||||
|
Number.isFinite(data.area_m2) && data.area_m2 > 0
|
||||||
|
? `${data.area_m2.toLocaleString("ru")} м²`
|
||||||
|
: "—",
|
||||||
mono: true,
|
mono: true,
|
||||||
},
|
},
|
||||||
{ label: "ВРИ", value: data.vri },
|
{ label: "ВРИ", value: data.vri },
|
||||||
|
|
|
||||||
|
|
@ -55,12 +55,18 @@ function buildCsvRows(data: ParcelAnalyzeResponse): string[][] {
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeCsvCell(cell: string): string {
|
function escapeCsvCell(cell: string): string {
|
||||||
// Wrap in quotes if contains comma, quote, or newline
|
// CSV-injection mitigation (OWASP / CWE-1236): cells starting with =, +, -, @,
|
||||||
const needsQuotes = /[",\n\r]/.test(cell);
|
// tab, or CR are evaluated as formulas by Excel/Calc/Sheets. Prefix dangerous
|
||||||
if (needsQuotes) {
|
// starters with a leading apostrophe so the spreadsheet treats them as text.
|
||||||
return `"${cell.replace(/"/g, '""')}"`;
|
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 {
|
function generateCsvBlob(rows: string[][]): Blob {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ interface Props {
|
||||||
function toSiteMapData(data: ParcelAnalyzeResponse): ParcelAnalysis {
|
function toSiteMapData(data: ParcelAnalyzeResponse): ParcelAnalysis {
|
||||||
return {
|
return {
|
||||||
cad_num: data.cad_num,
|
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,
|
geom_geojson: (data.geom_geojson as Geometry) ?? null,
|
||||||
score_breakdown: data.score_breakdown,
|
score_breakdown: data.score_breakdown,
|
||||||
score: data.score,
|
score: data.score,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ function buildFallbackEgrn(cad: string): ParcelEgrn {
|
||||||
return {
|
return {
|
||||||
cad_num: cad,
|
cad_num: cad,
|
||||||
address: "—",
|
address: "—",
|
||||||
area_m2: 0,
|
area_m2: NaN,
|
||||||
vri: "—",
|
vri: "—",
|
||||||
category: "—",
|
category: "—",
|
||||||
registration_date: null,
|
registration_date: null,
|
||||||
|
|
@ -57,7 +57,6 @@ function scoreVerdict(score: number, scoreLabel: string | undefined): string {
|
||||||
|
|
||||||
const labelUpper = label.toUpperCase();
|
const labelUpper = label.toUpperCase();
|
||||||
|
|
||||||
if (score >= 80) return `ПОДХОДИТ · ${labelUpper}`;
|
|
||||||
if (score >= 65) return `ПОДХОДИТ · ${labelUpper}`;
|
if (score >= 65) return `ПОДХОДИТ · ${labelUpper}`;
|
||||||
if (score >= 45) return `СРЕДНЕ · ${labelUpper}`;
|
if (score >= 45) return `СРЕДНЕ · ${labelUpper}`;
|
||||||
return `РИСКИ · ${labelUpper}`;
|
return `РИСКИ · ${labelUpper}`;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue