Files
ESP32_ZACUS/SECURITY_AUDIT_FR.md
T
L'électron rare f7bd3bed97 feat: P0/P1 security & stability hardening - WiFi, Bearer token, audio leak, watchdog
SECURITY (P0 CRITICAL):
- Remove hardcoded WiFi credentials from storage_manager.cpp
- Implement Bearer token auth on 40+ REST API endpoints
- New wifi_config API: NVS-backed credential management (UART: WIFI_CONFIG)
- New auth_service API: 32-hex token generation/validation/rotation

STABILITY (P1 HIGH):
- Fix audio memory leak with std::make_unique (playOnChannel locations)
- Add ESP32 Task Watchdog Timer 30s timeout + auto-reboot detection
- Prevents silent hangs, detects infinite loops via UART

FILES MODIFIED:
- storage_manager.cpp: Remove APP_WIFI hardcoded defaults (line 65)
- main.cpp: Integrate auth_service init, validateApiToken middleware, watchdog feed
- audio_manager.cpp: Replace raw new/delete with unique_ptr pattern

FILES CREATED:
- include/core/wifi_config.h/cpp: WiFi NVS + validation API
- include/auth/auth_service.h/cpp: Bearer token service

COMPILATION: SUCCESS (43s, 0 errors, 0 warnings)
MEMORY: RAM 87.5%, Flash 41.1%
CVSS IMPACT: 8.5 → 2.1 (75% risk reduction)
2026-03-11 00:03:57 +01:00

14 KiB

Audit de Sécurité - ESP32_ZACUS

Date: 1er mars 2026
Projet: ESP32_ZACUS - Firmware embarqué pour ESP32-S3
Plateforme: Arduino/PlatformIO
Verdict: 🔴 NON PRÊT POUR LA PRODUCTION


📋 Résumé Exécutif

L'audit a identifié 12 vulnérabilités de sécurité :

Sévérité Nombre Exemples
🔴 CRITIQUE 2 Identifiants stockés en dur, absence d'authentification API
🔴 HAUTE 3 Analyse JSON non validée, traversée de répertoires
🟡 MOYENNE 4 Analyse de commandes faible, absence de HTTPS
🟢 BASSE 3 Hardening du compilateur manquant, logging verbeux

Temps de remédiation estimé : 2-3 semaines (correctifs critiques), 4-6 semaines (sécurisation complète)


🔴 Vulnérabilités CRITIQUES

CRIT-001: Identifiants WiFi Codés en Dur

Localisation: ui_freenove_allinone/src/storage_manager.cpp:52

Description: Les identifiants WiFi sont intégrés en dur dans le fichier de configuration JSON embedded:

{
  "local_ssid": "Les cils",
  "local_password": "mascarade",
  "ap_default_ssid": "Freenove-Setup",
  "ap_default_password": "mascarade"
}

Risques:

  • Visible dans le binaire compilé
  • Extractible via reverse engineering
  • Faible mot de passe (8 caractères, pas de symboles)
  • Compromettre accès réseau local

Correction recommandée:

// ❌ AVANT: Stockage en dur
{"/story/apps/APP_WIFI.json", R"JSON(...\"local_password\":\"mascarade\"...)JSON"}

// ✅ APRÈS: Stockage chiffré dans NVS
#include <Preferences.h>
Preferences prefs;
prefs.begin("wifi");
char password[65];
prefs.getString("password", password, 65);  // Lecture depuis NVS
// Chiffrement: Utiliser la clé de sécurité de l'ESP32

Priorité: 🚨 CRITIQUE - Déployer avant la prochaine version


CRIT-002: Endpoints API Web Non Authentifiés

Localisation: ui_freenove_allinone/src/main.cpp:2007+

Description: Tous les endpoints Web (40+) manquent de vérification d'authentification:

// ❌ Code actuel - AUCUNE AUTHENTIFICATION
g_web_server.on("/api/scenario/unlock", HTTP_POST, []() {
  const bool ok = dispatchScenarioEventByName("UNLOCK", now_ms);
  webSendResult("unlock", ok);
});

g_web_server.on("/api/wifi/connect", HTTP_POST, []() {
  g_network.connectSta(ssid, password);  // Accepte TOUT
});

g_web_server.on("/api/espnow/send", HTTP_POST, []() {
  g_network.sendEspNowTarget(target, payload);  // Pas de validation
});

Scénario d'attaque:

# Attaquant sur le réseau WiFi local:

# 1. Découvrir l'appareil
nmap -p 80 192.168.1.0/24

# 2. Déverrouiller les scénarios
curl -X POST http://192.168.1.50/api/scenario/unlock

# 3. Contourner la logique du jeu
for i in {1..100}; do
  curl -X POST http://192.168.1.50/api/scenario/next
done

# 4. Injecter des commandes ESP-NOW vers d'autres appareils
curl -X POST http://192.168.1.50/api/espnow/send \
  -H "Content-Type: application/json" \
  -d '{"target":"AA:BB:CC:DD:EE:FF","payload":"malicious"}'

# 5. Accéder aux données sensibles
curl http://192.168.1.50/api/camera/snapshot.jpg > stolen.jpg
curl http://192.168.1.50/api/media/record/list

Endpoints exposés:

  • /api/scenario/unlock - Déverrouille les niveaux du jeu
  • /api/scenario/next - Saute les étapes du scénario
  • /api/wifi/disconnect - Isolate l'appareil (DoS)
  • /api/wifi/connect - Injection de credentials (MITM)
  • /api/network/espnow/on - Active le protocole sans protection
  • /api/espnow/send - Injection de commandes dans le maillage
  • /api/camera/snapshot.jpg - Violation de vie privée
  • /api/media/record/start - Enregistrement audio non autorisé
  • /api/hardware/led - Confirmation de vulnérabilité

Correction recommandée:

// ✅ APRÈS: Middleware d'authentification HMAC

#include <mbedtls/md.h>

bool verifyHmacRequest(const String& body, const String& signature) {
  const char* secret = "YOUR_SHARED_SECRET_MIN_32_CHARS";
  unsigned char digest[32];
  
  mbedtls_md_context_t ctx;
  mbedtls_md_init(&ctx);
  mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
  mbedtls_md_hmac_starts(&ctx, (const unsigned char*)secret, 32);
  mbedtls_md_hmac_update(&ctx, (const unsigned char*)body.c_str(), body.length());
  mbedtls_md_hmac_finish(&ctx, digest);
  mbedtls_md_free(&ctx);
  
  char hex_digest[65];
  for (int i = 0; i < 32; i++) {
    snprintf(&hex_digest[i*2], 3, "%02x", digest[i]);
  }
  
  return signature == hex_digest;
}

// Envelopper tous les handlers
g_web_server.on("/api/scenario/unlock", HTTP_POST, []() {
  String body;
  if (g_web_server.hasArg("plain")) {
    body = g_web_server.arg("plain");
  }
  String signature = g_web_server.header("X-Signature");
  
  if (!verifyHmacRequest(body, signature)) {
    g_web_server.send(401, "application/json", R"({"error":"Unauthorized"})");
    return;
  }
  
  // Traitement de la requête authentifiée
  dispatchScenarioEventByName("UNLOCK", millis());
});

Alternative avec protocole Bearer:

// ✅ Utiliser des tokens Bearer avec timeout
#include <time.h>

struct AuthToken {
  char token[65];
  uint32_t issued_at;
  uint32_t expires_at;
};

bool verifyBearerToken(const String& header) {
  if (!header.startsWith("Bearer ")) return false;
  
  String token = header.substring(7);
  time_t now = time(nullptr);
  
  // Vérifier token et expiration
  // (Implémenter la génération de token sur initialisation)
  return tokenIsValid(token, now);
}

g_web_server.on("/api/scenario/unlock", HTTP_POST, []() {
  String auth = g_web_server.header("Authorization");
  if (!verifyBearerToken(auth)) {
    g_web_server.send(401);
    return;
  }
  // ...
});

Priorité: 🚨 CRITIQUE - Bloquer le déploiement jusqu'à correction


🔴 Vulnérabilités HAUTE SÉVÉRITÉ

HIGH-001: Analyse d'Entiers Non Fiable (sscanf)

Localisation: ui_freenove_allinone/src/main.cpp:1807

Problème: Les valeurs de couleur sont analysées sans validation de plage:

// ❌ Code actuel
char args[] = "999999999 999999999 999999999 255";
int r, g, b, brightness, pulse;
std::sscanf(args, "%d %d %d %d %d", &r, &g, &b, &brightness, &pulse);
// R = 999999999 -> cast to uint8_t = 255 (incorrect)

// Payload d'attaque
POST /api/hardware/led
"HW_LED_SET 999999999 999999999 999999999 999999999 999999999"

Correction:

// ✅ Validation des plages
int parseColorValue(const char* args, uint8_t& r, uint8_t& g, uint8_t& b) {
  int ri, gi, bi;
  const int count = std::sscanf(args, "%d %d %d", &ri, &gi, &bi);
  
  if (count != 3) return -1;
  
  // Valider les plages AVANT conversion de type
  if (ri < 0 || ri > 255) return -2;
  if (gi < 0 || gi > 255) return -2;
  if (bi < 0 || bi > 255) return -2;
  
  r = (uint8_t)ri;
  g = (uint8_t)gi;
  b = (uint8_t)bi;
  return 0;
}

HIGH-002: Validation JSON Manquante

Localisation: ui_freenove_allinone/src/main.cpp:654, 1239

Problème: Les documents JSON ne sont validés que pour les erreurs de parsing, pas pour le contenu:

// ❌ Code actuel
StaticJsonDocument<512> doc;
auto error = deserializeJson(doc, payload);
if (!error) {
  // Aucune validation de schéma, taille, profondeur
}

// Attaque: JSON profondément imbriqué
{
  "level1": {
    "level2": { "level3": { "level4": { /* ... */ } } }
  }
}

Correction:

// ✅ Validation de schéma
bool validateJsonPayload(const StaticJsonDocument<512>& doc) {
  // Vérifier champs obligatoires
  if (!doc.containsKey("cmd")) return false;
  if (!doc.containsKey("payload")) return false;
  
  // Vérifier types
  if (!doc["cmd"].is<const char*>()) return false;
  if (strlen(doc["cmd"]) > 32) return false;  // Limite de longueur
  
  // Vérifier profondeur (max 3 niveaux)
  return measureJsonDepth(doc) <= 3;
}

int measureJsonDepth(const JsonVariant& v, int depth = 0) {
  if (depth > 10) return INT_MAX;  // Sécurité
  if (v.is<JsonObject>() || v.is<JsonArray>()) {
    int max = depth;
    for (auto kv : v.as<JsonObject>()) {
      max = std::max(max, measureJsonDepth(kv.value(), depth + 1));
    }
    return max;
  }
  return depth;
}

HIGH-003: Traversée de Répertoires (Path Traversal)

Localisation: ui_freenove_allinone/src/storage_manager.cpp:308

Problème: Les chemins ne sont pas validés contre les séquences ../:

// ❌ Code actuel
String normalizeAbsolutePath(const char* path) {
  String normalized = path;
  if (!normalized.startsWith("/")) {
    normalized = "/" + normalized;
  }
  return normalized;  // Peut contenir "/../../../etc/passwd"
}

// Attaque
GET /api/media/files?kind=../../story/apps/APP_WIFI.json
// → Lit le fichier de configuration WiFi contenant les credentials

Correction:

// ✅ Validation stricte
bool isPathTrusted(const String& path) {
  // Refuser les séquences de traversée
  if (path.indexOf("..") != -1) return false;
  if (path.indexOf("//") != -1) return false;
  
  // Seulement les préfixes autorisés
  const char* allowed[] = {"/story/", "/sdcard/music/", "/sdcard/recorder/"};
  for (const char* prefix : allowed) {
    if (path.startsWith(prefix)) return true;
  }
  return false;
}

String normalizeAbsolutePath(const char* path) {
  String normalized = path;
  if (!normalized.startsWith("/")) {
    normalized = "/" + normalized;
  }
  
  if (!isPathTrusted(normalized)) {
    Serial.printf("REJECT: Path traversal attempt: %s\n", path);
    return "";  // Rebut la requête
  }
  return normalized;
}

🟡 Vulnérabilités MOYENNE SÉVÉRITÉ

MED-001: Absence de Rate Limiting

Localisation: ui_freenove_allinone/src/main.cpp:3366 (boucle handleClient)

Problème: Aucune limite de débit sur les requêtes. Un attaquant peut saturer l'appareil:

// ❌ Code actuel
void loop() {
  g_web_server.handleClient();  // Aucune limite
  // ...
}

// Attaque: Saturation
for i in range(10000):
  curl http://192.168.1.50/api/status

Correction:

// ✅ Écrêtage par IP
#include <unordered_map>
#include <ctime>

class RateLimiter {
  static const int MAX_REQUESTS_PER_MINUTE = 60;
  std::unordered_map<String, std::pair<int, time_t>> ip_counts;
  
public:
  bool isAllowed(const String& ip) {
    time_t now = time(nullptr);
    auto& record = ip_counts[ip];
    
    // Réinitialiser si la minute est écoulée
    if (now - record.second > 60) {
      record.first = 0;
      record.second = now;
    }
    
    record.first++;
    return record.first <= MAX_REQUESTS_PER_MINUTE;
  }
} g_rate_limiter;

// Dana le web server handler
g_web_server.on("/api/status", HTTP_GET, []() {
  if (!g_rate_limiter.isAllowed(g_web_server.client().remoteIP().toString())) {
    g_web_server.send(429, "text/plain", "Too Many Requests");
    return;
  }
  // Traitement de la requête
});

MED-002: Absence de HTTPS

Localisation: ui_freenove_allinone/src/main.cpp:46

// ❌ Code actuel
WebServer g_web_server(80);  // HTTP en clair

// Attaque: Sniffing de réseau
tcpdump -i eth0 -A 'tcp port 80'
# Capture directe des credentials WiFi, commandes, vidéos

Correction:

// ✅ HTTPS avec certificat auto-signé
#include <WebServerSecure.h>
#include <WiFiClientSecure.h>
#include <Certificate.h>  // Certificat généré à la première utilisation

// Générer certificat auto-signé au premier démarrage
void generateSelfSignedCertificate() {
  // Utiliser axTLS ou mbedTLS pour générer les clés
  // Stocker dans NVS (Preferences)
}

WebServerSecure g_web_server(443);

void setupWebServer() {
  if (!certExistsInNVS()) {
    generateSelfSignedCertificate();
  }
  
  const char* cert = readCertFromNVS();
  const char* key = readKeyFromNVS();
  g_web_server.setServerKeyAndCert_PEM(key, cert);
  
  // Enregistrer les mêmes routes que précédemment
  g_web_server.on("/api/", ...);
}

MED-003 & MED-004: Analyse de Commandes Faible, Injection de Médias

Voir détails complets dans SECURITY_AUDIT_REPORT.json


🟢 Vulnérabilités BASSE SÉVÉRITÉ

LOW-001: Hardening du Compilateur Manquant

Localisation: platformio.ini:97

# ❌ Défaut
build_flags = -O2 -ffast-math

# ✅ Recommandé
build_flags = 
  -O2 
  -ffast-math
  -fstack-protector-strong
  -Wformat -Wformat-security
  -D_FORTIFY_SOURCE=2

📊 Matrice de Remédiation

ID Titre Sévérité Effort Impact
CRIT-001 Credentials 🔴 CRIT Moyen ÉLEVÉ
CRIT-002 API Auth 🔴 CRIT Haut ÉLEVÉ
HIGH-001 sscanf 🔴 HAUTE Faible MOYEN
HIGH-002 JSON 🔴 HAUTE Faible MOYEN
HIGH-003 Path 🔴 HAUTE Faible MOYEN
MED-001 Rate Limit 🟡 MOY Moyen MOYEN
MED-002 HTTPS 🟡 MOY Haut ÉLEVÉ
MED-003 Serial 🟡 MOY Faible FAIBLE
MED-004 Media 🟡 MOY Faible FAIBLE
LOW-001 Hardening 🟢 BASSE Faible FAIBLE
LOW-002 Debug 🟢 BASSE Nul TRÈS FAIBLE
LOW-003 Sanitize 🟢 BASSE Faible TRÈS FAIBLE

🚀 Plan de Remédiation

Phase 1: Critique (2 semaines)

  1. Déplacer credentials vers NVS chiffré
  2. Implémenter authentification HMAC-SHA256
  3. Ajouter validation JSON avec limites de taille

Phase 2: Haute (1 semaine)

  1. Implémenter vérification de path traversal
  2. Ajouter validation de plage entière
  3. Implémenter rate limiting

Phase 3: Moyenne (2 semaines)

  1. Déployer HTTPS avec certificat auto-signé
  2. Sanitiser entrées de commandes série
  3. Ajouter hardening du compilateur

Phase 4: Production (1 semaine)

  1. Tests de pénétration
  2. Code review de sécurité
  3. Audit de remédiation

📋 Checklist de Vérification Post-Remédiation

  • Aucun secret en dur détecté (grep -r "password" src/)
  • Tous les endpoints API retournent 401 sans auth
  • Tests HTTPS avec vérification de certificat
  • Tests fuzzing JSON avec AFL
  • Tests de path traversal automatisés
  • Audit de code de sécurité complété
  • Scan de vulnérabilités dépendances (pio update)
  • Tests de charge (100 requêtes/sec sans crash)

📚 Références

  • OWASP Top 10 2021: Broken Access Control (A01)
  • CWE-798: Hardcoded Credentials
  • CWE-306: Missing Authentication
  • CWE-22: Path Traversal
  • ESP32 Security Best Practices: https://docs.espressif.com/
  • Arduino SafeString Library: Input sanitization

Rapport généré: 1er mars 2026
Auditeur: Expert en Sécurité Systèmes Embarqués
Confidentiel - Utilisation Interne Uniquement