Skip to content

Latest commit

 

History

History
897 lines (671 loc) · 21.6 KB

File metadata and controls

897 lines (671 loc) · 21.6 KB

Guide de Sécurité - MiniSheet

📋 Table des Matières

  1. Introduction
  2. Vulnérabilités Identifiées
  3. Validation des Entrées
  4. Gestion des Fichiers
  5. Protection des Données
  6. Bonnes Pratiques
  7. Recommandations d'Amélioration
  8. Checklist de Sécurité
  9. FAQ

Introduction

Ce guide identifie les risques de sécurité de MiniSheet et propose des mesures pour améliorer la sécurité de l'application.

Objectifs de Sécurité

  • Validation stricte : Toutes les entrées utilisateur sont validées
  • Protection des fichiers : Prévention des attaques par fichiers malveillants
  • Limites de ressources : Protection contre les attaques DoS
  • Sécurité mémoire : Pas de vulnérabilités de mémoire
  • Confidentialité : Protection des données sensibles

Types de Menaces

  1. Injection de code : Formules malveillantes
  2. Déni de service (DoS) : Fichiers volumineux ou formules complexes
  3. Dépassement de mémoire : Consommation excessive de mémoire
  4. Fichiers malveillants : Fichiers JSON corrompus ou malformés
  5. Exposition de données : Données sensibles en texte clair

Vulnérabilités Identifiées

1. ⚠️ Pas de Limite sur la Taille des Formules

Problème :

Les formules peuvent être de n'importe quelle longueur, permettant des attaques DoS.

Code actuel :

fn eval_formula(&mut self, expr: &str, stack: &mut HashSet<(usize, usize)>) 
    -> Result<CellValue, CellValue> {
    let mut parser = FormulaParser::new(expr);
    // Pas de limite sur la longueur de expr
}

Risque :

  • Formule de plusieurs MB peut causer un crash ou consommation excessive de mémoire
  • Stack overflow avec formules très profondes

Solution :

const MAX_FORMULA_LENGTH: usize = 10_000;  // Limite raisonnable

fn eval_formula(&mut self, expr: &str, stack: &mut HashSet<(usize, usize)>) 
    -> Result<CellValue, CellValue> {
    if expr.len() > MAX_FORMULA_LENGTH {
        return Err(CellValue::Error("Formula too long".into()));
    }
    // ...
}

2. ⚠️ Pas de Limite sur la Profondeur de Récursion

Problème :

Le parser récursif peut avoir une profondeur de récursion illimitée.

Code actuel :

fn parse_expr(&mut self, ...) -> Result<f64, CellValue> {
    // Récursion illimitée possible
    let left = self.parse_additive(...)?;
    // ...
}

Risque :

  • Stack overflow avec formules très profondes
  • Crash de l'application

Solution :

struct FormulaParser<'a> {
    chars: Vec<char>,
    pos: usize,
    input: &'a str,
    recursion_depth: usize,  // Ajouter un compteur
}

const MAX_RECURSION_DEPTH: usize = 100;

impl<'a> FormulaParser<'a> {
    fn parse_expr(&mut self, ...) -> Result<f64, CellValue> {
        if self.recursion_depth > MAX_RECURSION_DEPTH {
            return Err(CellValue::Error("Formula too complex".into()));
        }
        self.recursion_depth += 1;
        // ...
        self.recursion_depth -= 1;
    }
}

3. ⚠️ Pas de Limite sur le Nombre de Cellules

Problème :

Un fichier peut contenir un nombre illimité de cellules, causant une consommation excessive de mémoire.

Code actuel :

struct Sheet {
    raw: HashMap<(usize, usize), String>,  // Pas de limite
    // ...
}

Risque :

  • Consommation excessive de mémoire
  • DoS par fichier volumineux

Solution :

const MAX_CELLS: usize = 1_000_000;  // Limite raisonnable

impl Sheet {
    fn set_raw(&mut self, r: usize, c: usize, value: String) {
        if self.raw.len() >= MAX_CELLS && !self.raw.contains_key(&(r, c)) {
            return;  // Refuser d'ajouter de nouvelles cellules
        }
        // ...
    }
}

4. ⚠️ Pas de Validation Stricte des Références de Cellules

Problème :

Les références de cellules ne sont pas validées strictement, permettant des références invalides.

Code actuel :

fn parse_cell_ref(s: &str) -> Option<(usize, usize)> {
    // Parse mais ne valide pas les limites
    // ...
}

Risque :

  • Références hors limites peuvent causer des erreurs
  • Comportement imprévisible

Solution :

fn parse_cell_ref(s: &str, max_rows: usize, max_cols: usize) -> Option<(usize, usize)> {
    let (r, c) = Self::parse_cell_ref_internal(s)?;
    if r >= max_rows || c >= max_cols {
        return None;  // Rejeter les références hors limites
    }
    Some((r, c))
}

5. ⚠️ Pas de Protection contre les Fichiers Malveillants

Problème :

Les fichiers JSON peuvent contenir des données malformées ou malveillantes.

Code actuel :

match serde_json::from_str::<Sheet>(&content) {
    Ok(sheet) => {
        self.sheet = sheet;  // Accepte n'importe quel JSON valide
    }
    // ...
}

Risques :

  • Fichiers avec des millions de cellules
  • Références circulaires profondes
  • Données corrompues

Solution :

fn open(&mut self) {
    // ...
    match serde_json::from_str::<Sheet>(&content) {
        Ok(sheet) => {
            // Valider le fichier avant de l'accepter
            if !self.validate_sheet(&sheet) {
                self.status = "File validation failed".into();
                return;
            }
            self.sheet = sheet;
        }
        // ...
    }
}

fn validate_sheet(&self, sheet: &Sheet) -> bool {
    // Vérifier les limites
    if sheet.raw.len() > MAX_CELLS {
        return false;
    }
    
    // Vérifier les dimensions
    if sheet.rows > MAX_ROWS || sheet.cols > MAX_COLS {
        return false;
    }
    
    // Vérifier les références de cellules
    for ((r, c), value) in &sheet.raw {
        if *r >= sheet.rows || *c >= sheet.cols {
            return false;
        }
        if value.len() > MAX_FORMULA_LENGTH {
            return false;
        }
    }
    
    true
}

6. ⚠️ Pas de Protection contre les Chaînes de Dépendances Longues

Problème :

Les chaînes de dépendances peuvent être très longues, causant des problèmes de performance.

Code actuel :

fn invalidate_cell(&mut self, r: usize, c: usize) {
    // Récursion illimitée possible
    for (dr, dc) in dependents {
        self.invalidate_cell(dr, dc);  // Récursion sans limite
    }
}

Risque :

  • Stack overflow avec chaînes très longues
  • Performance dégradée

Solution :

fn invalidate_cell(&mut self, r: usize, c: usize) {
    self.invalidate_cell_with_depth(r, c, 0);
}

fn invalidate_cell_with_depth(&mut self, r: usize, c: usize, depth: usize) {
    const MAX_INVALIDATION_DEPTH: usize = 1000;
    
    if depth > MAX_INVALIDATION_DEPTH {
        // Limiter la profondeur
        return;
    }
    
    self.computed.remove(&(r, c));
    
    let dependents: Vec<(usize, usize)> = /* ... */;
    for (dr, dc) in dependents {
        self.invalidate_cell_with_depth(dr, dc, depth + 1);
    }
}

7. ⚠️ Fichiers en Texte Clair

Problème :

Les fichiers JSON sont en texte clair, exposant les données sensibles.

Risque :

  • Données sensibles accessibles à quiconque peut lire le fichier
  • Pas de protection contre la lecture non autorisée

Solution :

  • Chiffrement optionnel des fichiers
  • Support de mots de passe
  • Chiffrement au niveau du système de fichiers

Validation des Entrées

Validation des Formules

Règles de validation :

  1. Longueur maximale : 10,000 caractères
  2. Profondeur de récursion : Maximum 100 niveaux
  3. Nombre de références : Maximum 1,000 références par formule
  4. Caractères autorisés : Seulement caractères ASCII imprimables et espaces

Implémentation :

fn validate_formula(formula: &str) -> Result<(), String> {
    const MAX_LENGTH: usize = 10_000;
    const MAX_REFERENCES: usize = 1_000;
    
    // Vérifier la longueur
    if formula.len() > MAX_LENGTH {
        return Err("Formula too long".into());
    }
    
    // Vérifier les caractères
    if !formula.chars().all(|c| c.is_ascii() && (c.is_alphanumeric() || 
        "=+-*/^()<>:,. \t\n".contains(c))) {
        return Err("Invalid characters in formula".into());
    }
    
    // Compter les références
    let ref_count = formula.matches(char::is_alphabetic).count();
    if ref_count > MAX_REFERENCES {
        return Err("Too many cell references".into());
    }
    
    Ok(())
}

Validation des Valeurs de Cellules

Règles :

  1. Longueur maximale : 1,000,000 caractères
  2. Nombres : Vérifier qu'ils sont finis (pas NaN, pas Infinity)
  3. Texte : Limiter la taille pour éviter DoS

Implémentation :

fn validate_cell_value(value: &str) -> Result<(), String> {
    const MAX_VALUE_LENGTH: usize = 1_000_000;
    
    if value.len() > MAX_VALUE_LENGTH {
        return Err("Cell value too long".into());
    }
    
    // Si c'est un nombre, vérifier qu'il est valide
    if let Ok(n) = value.trim().parse::<f64>() {
        if !n.is_finite() {
            return Err("Invalid number (NaN or Infinity)".into());
        }
    }
    
    Ok(())
}

Validation des Coordonnées

Règles :

  1. Limites : rows < 1,000,000, cols < 10,000
  2. Vérification : Toujours valider avant d'accéder

Implémentation :

const MAX_ROWS: usize = 1_000_000;
const MAX_COLS: usize = 10_000;

fn is_valid_cell(&self, r: usize, c: usize) -> bool {
    r < self.rows.min(MAX_ROWS) && c < self.cols.min(MAX_COLS)
}

Gestion des Fichiers

Validation des Fichiers

Vérifications à effectuer :

  1. Taille du fichier : Maximum 100 MB
  2. Structure JSON : Valide et conforme au schéma
  3. Nombre de cellules : Maximum 1,000,000
  4. Références : Toutes les références doivent être valides

Implémentation :

fn validate_file(path: &Path) -> Result<(), String> {
    // Vérifier la taille
    let metadata = fs::metadata(path)?;
    const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;  // 100 MB
    if metadata.len() > MAX_FILE_SIZE {
        return Err("File too large".into());
    }
    
    // Lire et valider le contenu
    let content = fs::read_to_string(path)?;
    let sheet: Sheet = serde_json::from_str(&content)?;
    
    // Valider la structure
    validate_sheet_structure(&sheet)?;
    
    Ok(())
}

fn validate_sheet_structure(sheet: &Sheet) -> Result<(), String> {
    const MAX_CELLS: usize = 1_000_000;
    const MAX_ROWS: usize = 1_000_000;
    const MAX_COLS: usize = 10_000;
    
    // Vérifier les dimensions
    if sheet.rows > MAX_ROWS || sheet.cols > MAX_COLS {
        return Err("Sheet dimensions too large".into());
    }
    
    // Vérifier le nombre de cellules
    if sheet.raw.len() > MAX_CELLS {
        return Err("Too many cells".into());
    }
    
    // Vérifier toutes les cellules
    for ((r, c), value) in &sheet.raw {
        if *r >= sheet.rows || *c >= sheet.cols {
            return Err(format!("Invalid cell coordinates: ({}, {})", r, c));
        }
        
        if value.len() > 10_000 {
            return Err("Cell value too long".into());
        }
    }
    
    Ok(())
}

Protection contre Path Traversal

Problème :

Les attaquants pourraient utiliser ../ pour accéder à des fichiers en dehors du répertoire prévu.

Solution :

use std::path::{Path, PathBuf};

fn sanitize_path(path: &Path, base_dir: &Path) -> Result<PathBuf, String> {
    // Normaliser le chemin
    let normalized = path.canonicalize()
        .map_err(|_| "Invalid path")?;
    
    // Vérifier qu'il est dans le répertoire de base
    if !normalized.starts_with(base_dir) {
        return Err("Path traversal detected".into());
    }
    
    Ok(normalized)
}

Limitation de la Taille des Fichiers

Implémentation :

fn open(&mut self) {
    if let Some(path) = rfd::FileDialog::new()
        .add_filter("MiniSheet", &["json"])
        .pick_file()
    {
        // Vérifier la taille avant de charger
        if let Ok(metadata) = fs::metadata(&path) {
            const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;  // 100 MB
            if metadata.len() > MAX_FILE_SIZE {
                self.status = "File too large (max 100 MB)".into();
                self.status_type = StatusType::Error;
                return;
            }
        }
        
        // Charger le fichier
        // ...
    }
}

Protection des Données

Chiffrement Optionnel

Recommandation :

Ajouter un support optionnel pour le chiffrement des fichiers.

Implémentation (exemple avec aes-gcm) :

// Dans Cargo.toml
// aes-gcm = "0.10"

use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead};

fn save_encrypted(&mut self, path: &PathBuf, password: &str) -> Result<(), String> {
    // Sérialiser les données
    let json = serde_json::to_string(&self.sheet)?;
    
    // Chiffrer avec le mot de passe
    let key = derive_key(password);
    let cipher = Aes256Gcm::new(&key);
    let nonce = generate_nonce();
    let ciphertext = cipher.encrypt(&nonce, json.as_bytes())
        .map_err(|_| "Encryption failed")?;
    
    // Sauvegarder
    let encrypted_data = EncryptedFile {
        nonce,
        ciphertext,
    };
    fs::write(path, serde_json::to_string(&encrypted_data)?)?;
    
    Ok(())
}

Hachage pour Intégrité

Recommandation :

Ajouter un checksum pour détecter la corruption.

Implémentation :

use sha2::{Sha256, Digest};

fn save_with_checksum(&mut self, path: &PathBuf) -> Result<(), String> {
    let json = serde_json::to_string(&self.sheet)?;
    
    // Calculer le hash
    let mut hasher = Sha256::new();
    hasher.update(json.as_bytes());
    let hash = hasher.finalize();
    
    // Sauvegarder avec le hash
    let file_data = FileData {
        data: json,
        checksum: format!("{:x}", hash),
    };
    
    fs::write(path, serde_json::to_string(&file_data)?)?;
    Ok(())
}

fn load_with_checksum(&mut self, path: &PathBuf) -> Result<(), String> {
    let content = fs::read_to_string(path)?;
    let file_data: FileData = serde_json::from_str(&content)?;
    
    // Vérifier le checksum
    let mut hasher = Sha256::new();
    hasher.update(file_data.data.as_bytes());
    let hash = format!("{:x}", hasher.finalize());
    
    if hash != file_data.checksum {
        return Err("File integrity check failed".into());
    }
    
    // Charger les données
    self.sheet = serde_json::from_str(&file_data.data)?;
    Ok(())
}

Bonnes Pratiques

1. Toujours Valider les Entrées

❌ Mauvais :

fn set_raw(&mut self, r: usize, c: usize, value: String) {
    self.raw.insert((r, c), value);  // Pas de validation
}

✅ Bon :

fn set_raw(&mut self, r: usize, c: usize, value: String) -> Result<(), String> {
    // Valider les coordonnées
    if !self.is_valid_cell(r, c) {
        return Err("Invalid cell coordinates".into());
    }
    
    // Valider la valeur
    validate_cell_value(&value)?;
    
    // Valider la formule si c'est une formule
    if value.trim_start().starts_with('=') {
        validate_formula(&value[1..])?;
    }
    
    self.raw.insert((r, c), value);
    Ok(())
}

2. Utiliser Result au lieu de Paniquer

❌ Mauvais :

let value = self.raw.get(&(r, c)).unwrap();  // Panic si absent

✅ Bon :

match self.raw.get(&(r, c)) {
    Some(value) => Ok(value.clone()),
    None => Err(CellValue::Empty),
}

3. Limiter les Ressources

❌ Mauvais :

fn recalc_all(&mut self) {
    // Recalcule tout sans limite
    for (r, c) in self.raw.keys() {
        // ...
    }
}

✅ Bon :

const MAX_RECALC_TIME_MS: u64 = 5000;  // 5 secondes max

fn recalc_all(&mut self) -> Result<(), String> {
    let start = Instant::now();
    
    for (r, c) in self.raw.keys() {
        if start.elapsed().as_millis() > MAX_RECALC_TIME_MS {
            return Err("Recalculation timeout".into());
        }
        // ...
    }
    
    Ok(())
}

4. Sanitizer les Chemins de Fichiers

❌ Mauvais :

fs::write(path, data)?;  // Pas de validation du chemin

✅ Bon :

fn save_to_path(&mut self, path: &PathBuf) -> Result<(), String> {
    // Valider le chemin
    let sanitized = sanitize_path(path, &self.base_directory)?;
    
    // Vérifier les permissions
    // ...
    
    fs::write(&sanitized, data)?;
    Ok(())
}

5. Limiter la Profondeur de Récursion

❌ Mauvais :

fn invalidate_cell(&mut self, r: usize, c: usize) {
    // Récursion illimitée
    for dependent in dependents {
        self.invalidate_cell(dependent.0, dependent.1);
    }
}

✅ Bon :

fn invalidate_cell(&mut self, r: usize, c: usize) {
    self.invalidate_cell_with_depth(r, c, 0);
}

fn invalidate_cell_with_depth(&mut self, r: usize, c: usize, depth: usize) {
    const MAX_DEPTH: usize = 1000;
    
    if depth > MAX_DEPTH {
        eprintln!("Warning: Invalidation depth exceeded");
        return;
    }
    
    // ...
}

Recommandations d'Amélioration

1. Ajouter des Limites Strictes

Priorité : HAUTE

// Constantes de sécurité
const MAX_FORMULA_LENGTH: usize = 10_000;
const MAX_RECURSION_DEPTH: usize = 100;
const MAX_CELLS: usize = 1_000_000;
const MAX_ROWS: usize = 1_000_000;
const MAX_COLS: usize = 10_000;
const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;  // 100 MB
const MAX_VALUE_LENGTH: usize = 1_000_000;
const MAX_INVALIDATION_DEPTH: usize = 1_000;

2. Validation Complète des Entrées

Priorité : HAUTE

  • Valider toutes les formules avant évaluation
  • Valider toutes les valeurs de cellules
  • Valider toutes les références de cellules
  • Valider tous les fichiers avant chargement

3. Protection contre DoS

Priorité : MOYENNE

  • Limiter le temps de recalcul
  • Limiter la profondeur de récursion
  • Limiter la taille des fichiers
  • Limiter le nombre de cellules

4. Chiffrement Optionnel

Priorité : BASSE

  • Support de mots de passe pour les fichiers
  • Chiffrement AES-256
  • Hachage pour intégrité

5. Audit de Sécurité

Priorité : MOYENNE

  • Revue de code pour vulnérabilités
  • Tests de sécurité
  • Analyse statique avec cargo audit

Checklist de Sécurité

Validation

  • Limite sur la longueur des formules
  • Limite sur la profondeur de récursion
  • Limite sur le nombre de cellules
  • Validation des références de cellules
  • Validation des valeurs de cellules
  • Validation des fichiers avant chargement

Protection des Fichiers

  • Limite sur la taille des fichiers
  • Validation de la structure JSON
  • Protection contre path traversal
  • Vérification d'intégrité (checksum)

Protection des Données

  • Chiffrement optionnel
  • Hachage pour intégrité
  • Pas de données sensibles en texte clair (si possible)

Gestion des Erreurs

  • Pas de unwrap() sur les entrées utilisateur
  • Gestion d'erreurs explicite avec Result
  • Messages d'erreur informatifs mais non révélateurs

Limites de Ressources

  • Limite sur la mémoire
  • Limite sur le temps de calcul
  • Limite sur la profondeur de récursion
  • Timeout pour les opérations longues

FAQ

Q: MiniSheet est-il sûr à utiliser ?

R: MiniSheet est relativement sûr pour un usage normal, mais il y a des améliorations de sécurité à apporter, notamment :

  • Limites sur les ressources
  • Validation stricte des entrées
  • Protection contre les fichiers malveillants

Q: Les fichiers sont-ils sécurisés ?

R: Les fichiers JSON sont en texte clair. Pour des données sensibles :

  • Utilisez le chiffrement au niveau du système de fichiers
  • Ne partagez pas de fichiers contenant des informations sensibles
  • Considérez l'ajout de chiffrement dans MiniSheet

Q: Puis-je être attaqué via un fichier malveillant ?

R: Potentiellement oui, si le fichier contient :

  • Des millions de cellules (DoS)
  • Des formules très complexes (stack overflow)
  • Des références circulaires profondes

Protection : Validez toujours les fichiers avant de les ouvrir.

Q: Comment protéger mes données sensibles ?

R:

  1. Ne stockez pas de données sensibles dans MiniSheet si possible
  2. Utilisez le chiffrement au niveau du système de fichiers
  3. Limitez l'accès aux fichiers
  4. Considérez l'ajout de chiffrement dans MiniSheet

Q: Y a-t-il des vulnérabilités connues ?

R: Vulnérabilités identifiées :

  • Pas de limite sur la taille des formules
  • Pas de limite sur la profondeur de récursion
  • Pas de limite sur le nombre de cellules
  • Pas de validation stricte des fichiers

Toutes ces vulnérabilités peuvent être exploitées pour des attaques DoS.


Résumé

Vulnérabilités Identifiées

  1. ⚠️ Pas de limite sur la taille des formules
  2. ⚠️ Pas de limite sur la profondeur de récursion
  3. ⚠️ Pas de limite sur le nombre de cellules
  4. ⚠️ Pas de validation stricte des références
  5. ⚠️ Pas de protection contre les fichiers malveillants
  6. ⚠️ Pas de protection contre les chaînes de dépendances longues
  7. ⚠️ Fichiers en texte clair

Recommandations Prioritaires

HAUTE PRIORITÉ :

  1. Ajouter des limites strictes sur toutes les ressources
  2. Valider toutes les entrées utilisateur
  3. Valider tous les fichiers avant chargement

MOYENNE PRIORITÉ : 4. Protection contre DoS 5. Audit de sécurité

BASSE PRIORITÉ : 6. Chiffrement optionnel 7. Hachage pour intégrité

Bonnes Pratiques

  • ✅ Toujours valider les entrées
  • ✅ Utiliser Result au lieu de paniquer
  • ✅ Limiter les ressources
  • ✅ Sanitizer les chemins de fichiers
  • ✅ Limiter la profondeur de récursion

Dernière mise à jour : 2026-01-20

Version : 0.1.0