- Introduction
- Vulnérabilités Identifiées
- Validation des Entrées
- Gestion des Fichiers
- Protection des Données
- Bonnes Pratiques
- Recommandations d'Amélioration
- Checklist de Sécurité
- FAQ
Ce guide identifie les risques de sécurité de MiniSheet et propose des mesures pour améliorer la sécurité de l'application.
- ✅ 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
- Injection de code : Formules malveillantes
- Déni de service (DoS) : Fichiers volumineux ou formules complexes
- Dépassement de mémoire : Consommation excessive de mémoire
- Fichiers malveillants : Fichiers JSON corrompus ou malformés
- Exposition de données : Données sensibles en texte clair
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()));
}
// ...
}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;
}
}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
}
// ...
}
}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))
}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
}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);
}
}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
Règles de validation :
- Longueur maximale : 10,000 caractères
- Profondeur de récursion : Maximum 100 niveaux
- Nombre de références : Maximum 1,000 références par formule
- 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(())
}Règles :
- Longueur maximale : 1,000,000 caractères
- Nombres : Vérifier qu'ils sont finis (pas NaN, pas Infinity)
- 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(())
}Règles :
- Limites : rows < 1,000,000, cols < 10,000
- 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)
}Vérifications à effectuer :
- Taille du fichier : Maximum 100 MB
- Structure JSON : Valide et conforme au schéma
- Nombre de cellules : Maximum 1,000,000
- 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(())
}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)
}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
// ...
}
}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(())
}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(())
}❌ 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(())
}❌ 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),
}❌ 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(())
}❌ 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(())
}❌ 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;
}
// ...
}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;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
Priorité : MOYENNE
- Limiter le temps de recalcul
- Limiter la profondeur de récursion
- Limiter la taille des fichiers
- Limiter le nombre de cellules
Priorité : BASSE
- Support de mots de passe pour les fichiers
- Chiffrement AES-256
- Hachage pour intégrité
Priorité : MOYENNE
- Revue de code pour vulnérabilités
- Tests de sécurité
- Analyse statique avec
cargo audit
- 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
- Limite sur la taille des fichiers
- Validation de la structure JSON
- Protection contre path traversal
- Vérification d'intégrité (checksum)
- Chiffrement optionnel
- Hachage pour intégrité
- Pas de données sensibles en texte clair (si possible)
- Pas de
unwrap()sur les entrées utilisateur - Gestion d'erreurs explicite avec
Result - Messages d'erreur informatifs mais non révélateurs
- Limite sur la mémoire
- Limite sur le temps de calcul
- Limite sur la profondeur de récursion
- Timeout pour les opérations longues
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
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
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.
R:
- Ne stockez pas de données sensibles dans MiniSheet si possible
- Utilisez le chiffrement au niveau du système de fichiers
- Limitez l'accès aux fichiers
- Considérez l'ajout de chiffrement dans MiniSheet
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.
⚠️ 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 références⚠️ Pas de protection contre les fichiers malveillants⚠️ Pas de protection contre les chaînes de dépendances longues⚠️ Fichiers en texte clair
HAUTE PRIORITÉ :
- Ajouter des limites strictes sur toutes les ressources
- Valider toutes les entrées utilisateur
- 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é
- ✅ Toujours valider les entrées
- ✅ Utiliser
Resultau 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