From 529556fd1728dadd68584b7526eb660e75f21152 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 16:38:46 +0300 Subject: [PATCH] security: escape formula-like price csv cells --- docs/quality/uygulama-durumu.md | 5 +++++ src/components/tools/PriceAnalysis.jsx | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 5b83ed9..3cb2e30 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -1365,6 +1365,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö - Fiyat analizi CSV dışa aktarımında min/medyan/ortalama/maksimum değerleri artık yuvarlanıp kuruş kaybetmiyor; iki ondalık basamakla aynı ham istatistikleri taşıyor. - Ekrandaki yuvarlanmış grafik sunumu korunuyor; indirilen rapor hesaplama denetimi için daha hassas değer içeriyor. 283 test ve build başarılı. CSV’nin gerçek tarayıcıda indirilip açılması henüz doğrulanmadı; genel plan açık. +### 2026-09-06 — Fiyat analizi CSV formül güvenliği + +- CSV dışa aktarımında ürün adı, birim ve diğer hücreler artık Excel formülü başlangıç karakterlerine karşı kaçışlanıyor; `=`, `+`, `-`, `@` ile başlayan kaynak metinler çalışma kitabı formülü olarak çalıştırılmıyor. +- Tüm alanlar tırnaklanıp çift tırnaklar kaçışlanıyor; kuruş hassasiyeti korunuyor. 283 test ve build başarılı. CSV’nin gerçek Excel/LibreOffice açılış kabulü henüz yapılmadı; genel plan açık. + ### 2026-09-06 — Belge son kullanma tarihi doğrulaması - Belge deposundaki son kullanma tarihi göstergesi artık yalnız geçerli ISO günlerini hesaplıyor. Bozuk veya yuvarlanabilir bir tarih sessizce gün hesabına girmiyor; kullanıcıya “Tarih doğrulanmalı” uyarısı gösteriliyor. diff --git a/src/components/tools/PriceAnalysis.jsx b/src/components/tools/PriceAnalysis.jsx index 0e6a47d..889003e 100644 --- a/src/components/tools/PriceAnalysis.jsx +++ b/src/components/tools/PriceAnalysis.jsx @@ -6,6 +6,11 @@ import { itemStats } from '../../services/analytics'; import confetti from 'canvas-confetti'; const formatPrice = (n) => n == null || !Number.isFinite(Number(n)) ? '—' : Number(n).toLocaleString('tr-TR', {minimumFractionDigits:2, maximumFractionDigits:2}) + ' ₺'; +const csvCell = value => { + const text = String(value ?? ''); + const safe = /^[=+\-@]/.test(text) ? `'${text}` : text; + return `"${safe.replace(/"/g, '""')}"`; +}; export const PriceAnalysis = () => { const { tenders } = useApp(); @@ -37,8 +42,8 @@ export const PriceAnalysis = () => { if (!items.length) return; const headers = ['Kalem', 'Birim', 'Adet(kayıt)', 'Min', 'Medyan', 'Ortalama', 'Maks']; const rows = items.map((i) => [ - `"${(i.ad || '').replace(/"/g, '""')}"`, i.birim || '', i.count, - i.min.toFixed(2), i.median.toFixed(2), i.avg.toFixed(2), i.max.toFixed(2), + csvCell(i.ad), csvCell(i.birim), csvCell(i.count), + csvCell(i.min.toFixed(2)), csvCell(i.median.toFixed(2)), csvCell(i.avg.toFixed(2)), csvCell(i.max.toFixed(2)), ]); const csv = 'data:text/csv;charset=utf-8,' + [headers.join(','), ...rows.map((r) => r.join(','))].join('\n'); const link = document.createElement('a');