vgv-static-security

Par verygoodopensource · vgv-ai-flutter-plugin

Bonnes pratiques de sécurité pour les applications mobiles Flutter. Couvre les aspects de sécurité statiques — exclut les tests d'intrusion et l'analyse à l'exécution.

npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill vgv-static-security

Sécurité

Les applications Flutter compilent tout le code Dart directement en un binaire qui s'exécute sur des appareils non fiables. Cette compétence couvre l'examen de sécurité statique pour les bases de code Flutter/Dart, ancrée au guide VGV Security in Mobile Apps et aux OWASP Mobile Top 10. Chaque constat dans cette compétence est quelque chose de détectable en lisant le code source — pas de pen-testing ou analyse en runtime.

Standards Fondamentaux

Appliquez ces standards à TOUS les travaux de sécurité Flutter :

  • Ne jamais coder les secrets en dur — les clés API, tokens et mots de passe dans le code source ou les fichiers de config sont compilés dans le binaire et extractibles par ingénierie inverse ; servez-les depuis un service backend
  • Utilisez package:flutter_secure_storage pour les données sensibles stockées localementSharedPreferences est en texte brut et non chiffré ; ne stockez jamais les tokens, les données personnelles ou les données de session là
  • Tous les appels réseau sur HTTPS — HTTP brut transmet les données en clair ; ne désactivez jamais la validation de certificat (la seule exception est pendant le développement avec un serveur de test local)
  • Utilisez Random.secure() pour la génération aléatoire sensible à la sécuritéRandom() de dart:math est un générateur de nombres pseudo-aléatoires, pas cryptographiquement sûr
  • Utilisez des packages crypto établis — ne mettez jamais en place de cryptographie personnalisée ; utilisez package:crypto ou package:dart_crypt
  • Appliquez l'auth au niveau de la couche repository — les vérifications d'auth au niveau du widget sont côté client et contournables par quiconque a accès à l'appareil
  • Pas de données sensibles dans les logs — la sortie de print(), log() et debugPrint() est lisible sur l'appareil et dans les outils de rapports d'crash
  • Gardez les dépendances libres des vulnérabilités connues — ne supprimez jamais les avis de sécurité sans justification documentée ; scannez pubspec.lock avec osv-scanner avant chaque release
  • Définissez android:allowBackup="false" — la valeur par défaut d'Android permet silencieusement à adb backup d'extraire les données de l'app, contournant package:flutter_secure_storage

Secrets & Clés API

Les clés API, tokens et identifiants codés en dur dans les fichiers source ou les fichiers de config groupés sont extractibles du binaire compilé par ingénierie inverse. Chaque secret doit être servi par un service backend à l'exécution.

Fichiers à vérifier : fichiers source Dart, google-services.json, .env, *.plist, AndroidManifest.xml, Info.plist.

// ❌ Clé API codée en dur — extractible du binaire
const apiKey = 'sk-abc123';
const mapboxToken = 'pk.your-token-here';

// ❌ Secret dans la config — groupé dans l'app
// google-services.json:
// "api_key": [{ "current_key": "AIzaSy..." }]
// ✅ Récupérée d'un service backend à l'exécution — la seule option sûre
final apiKey = await secretsService.fetchApiKey();

Ne commitez jamais les fichiers .env ou les fichiers contenant de vrais identifiants dans le contrôle de version. Utilisez .gitignore pour les exclure et un service de gestion des secrets à la place. Remarque : --dart-define / String.fromEnvironment compilent les valeurs dans le binaire en texte brut et sont extractibles par ingénierie inverse — ce ne sont pas une alternative sûre aux secrets servis par le backend.

Stockage Sécurisé des Données

Les données sensibles écrites sur l'appareil doivent être chiffrées. iOS Keychain et Android Keystore fournissent un stockage chiffré soutenu par le matériel — package:flutter_secure_storage encapsule les deux.

// ❌ JWT stocké dans SharedPreferences — texte brut, non chiffré
final prefs = await SharedPreferences.getInstance();
prefs.setString('auth_token', jwt);

// ❌ Valeur sensible dans un fichier local — pas de chiffrement
await File('${dir.path}/user.json').writeAsString(jsonEncode(user));
// ✅ package:flutter_secure_storage — soutenu par iOS Keychain / Android Keystore
const storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: jwt);
final token = await storage.read(key: 'auth_token');
await storage.delete(key: 'auth_token');

Utilisez SharedPreferences uniquement pour les préférences utilisateur non sensibles (thème, locale, état d'onboarding). Ne stockez jamais les mots de passe, les tokens de session, les données personnelles ou les clés privées là.

Sécurité Réseau

Toute communication entre une app Flutter et un backend doit être chiffrée en transit. HTTP brut expose les données à l'interception sur n'importe quel réseau auquel l'utilisateur se connecte.

// ❌ URL de base HTTP brut
final dio = Dio(BaseOptions(baseUrl: 'http://api.example.com'));

// ❌ Validation de certificat désactivée — vulnérable aux attaques MITM
final client = HttpClient()
  ..badCertificateCallback = (cert, host, port) => true;
// ✅ URL de base HTTPS
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

Implémentez l'épinglage de certificat (package:http_certificate_pinning) pour les endpoints qui gèrent l'authentification, les paiements ou les données personnelles. N'acceptez que les certificats signés par l'autorité de certification attendue.

Authentification

Les contrôles d'authentification doivent être appliqués côté serveur. Les vérifications côté client (dans les widgets ou le routage) sont des commodités UI uniquement — elles peuvent être contournées par quiconque ayant un accès physique ou un accès au debugger à l'appareil.

Application côté serveur : le serveur doit valider le token à chaque requête. Une réponse 401 de l'API est la porte d'authentification autoritaire — pas une condition widget.

Authentification biométrique : utilisez package:local_auth pour l'accès biométrique aux flux sensibles dans l'app — n'appelez pas directement les channels de plateforme.

Utilisez Firebase Authentication ou Auth0 pour la gestion des identifiants — ne construisez pas de flux d'authentification personnalisés.

Cryptographie

Les implémentations cryptographiques personnalisées contiennent presque toujours des bugs subtils. Utilisez des packages relus par les pairs et évitez les algorithmes faibles ou dépréciés.

// ❌ Aléatoire cryptographiquement non sûr — Random de dart:math n'est pas CSPRNG
import 'dart:math';
final sessionId = Random().nextInt(1 << 32).toRadixString(16);
final iv = List.generate(16, (_) => Random().nextInt(256));

// ❌ Algorithme de hash faible — MD5 et SHA-1 sont cassés pour l'usage en sécurité
import 'dart:convert';
final hash = md5.convert(utf8.encode(password)).toString();

// ❌ Clé de chiffrement codée en dur
const encryptionKey = 'my-secret-key-123';
// ✅ Aléatoire cryptographiquement sûr — Random.secure()
import 'dart:math';
final sessionId = Random.secure().nextInt(1 << 32).toRadixString(16);
final iv = List.generate(16, (_) => Random.secure().nextInt(256));

// ✅ Hash fort via package:crypto
import 'package:crypto/crypto.dart';
import 'dart:convert';
final hash = sha256.convert(utf8.encode(data)).toString();

// ✅ Clé de chiffrement depuis le stockage sécurisé, pas le code source
final key = await storage.read(key: 'encryption_key');

À éviter : MD5, SHA-1, DES, RC4, mode ECB. Préférer : SHA-256+ pour le hash, AES-GCM pour le chiffrement, SHA-512-crypt pour le stockage de mots de passe.

Validation des Entrées

Toutes les données provenant des entrées utilisateur doivent être validées avant d'atteindre un repository ou une API. Les valeurs brutes de TextEditingController.text envoyées directement à un backend sont un risque d'injection et peuvent soumettre des données mal formées.

// ❌ Texte du contrôleur brut envoyé directement à l'API
ElevatedButton(
  onPressed: () => context.read<AuthBloc>().add(
    LoginRequested(
      email: _emailController.text,
      password: _passwordController.text,
    ),
  ),
  child: const Text('Login'),
);
// ✅ Valeurs FormzInput validées — seules les données valides atteignent le Bloc
class Email extends FormzInput<String, EmailValidationError> {
  const Email.pure() : super.pure('');
  const Email.dirty([super.value = '']) : super.dirty();

  @override
  EmailValidationError? validator(String value) {
    final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
    if (value.isEmpty) return EmailValidationError.empty;
    if (!emailRegex.hasMatch(value)) return EmailValidationError.invalid;
    return null;
  }
}

// Dans le widget — soumettre seulement quand le formulaire est valide
if (state.status.isValidated) {
  context.read<AuthBloc>().add(
    LoginRequested(email: state.email.value, password: state.password.value),
  );
}

Utilisez package:formz pour toute validation de formulaire. Définissez une sous-classe FormzInput par champ avec des règles de validation explicites et des limites de longueur.

Logging & Exposition d'Erreurs

La sortie de log est lisible via le débogage USB, les SDK de rapports de crash et l'analytics des appareils. Les valeurs sensibles qui apparaissent dans les logs sont effectivement transmises à n'importe quel outil connecté à l'appareil.

// ❌ Token dans la sortie de log
debugPrint('Auth token: $token');
log('User data: ${jsonEncode(user)}');
print('Request headers: $headers'); // les headers peuvent contenir les tokens Bearer

// ❌ Message d'exception expose les détails internes à l'UI
catch (e) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(e.toString())), // peut inclure les stack traces ou SQL
  );
}
// ✅ Logger seulement les identifiants non sensibles
debugPrint('Login attempt for userId: ${user.id}');

// ✅ Assainir les messages d'exception avant de les exposer à l'UI
catch (e, stackTrace) {
  log('Login failed', error: e, stackTrace: stackTrace); // détail complet pour les outils de crash
  emit(state.copyWith(status: LoginStatus.failure)); // message générique pour l'UI
}

Ne loggez jamais : les tokens, les mots de passe, les objets utilisateur complets, les en-têtes de requête HTTP (qui contiennent Authorization), ou les données personnelles (email, téléphone, numéro de sécurité sociale).

Vulnérabilités de Dépendances

Les packages tiers sont compilés directement dans le binaire de l'app. Un package vulnérable ou malveillant affecte chaque utilisateur sur chaque plateforme. Ceci est OWASP Mobile Top 10 M2 (Inadequate Supply Chain Security).

  • Exécutez dart pub get pour surfacer les hits de la GitHub Advisory Database
  • Toute entrée ignored_advisories dans pubspec.yaml doit avoir un commentaire de justification documenté
  • Scannez pubspec.lock avec osv-scanner avant chaque release
  • Exécutez dart pub outdated pour vérifier les patches de sécurité disponibles

Voir references/supply-chain.md pour des exemples de détection d'advisory, l'installation de osv-scanner, les signaux de typosquatting et les vérifications de creep des permissions transitives. Voir references/binary-protection.md pour l'obfuscation, la sauvegarde Android et l'intégrité runtime.

Ressources Supplémentaires

Voir references/packages.md pour la référence rapide des packages et le guide de triage de sévérité. Voir references/crypto.md pour l'implémentation de l'épinglage de certificat, un exemple d'authentification biométrique et le hash de mots de passe avec package:dart_crypt.

Skills similaires