Sécurité des applications C++ : top 10 des vulnérabilités en pratique
C++ est un langage puissant et performant, mais sa flexibilité exige une grande responsabilité de la part du développeur. Les erreurs de gestion de la mémoire, de travail avec les threads ou d'entrée-sortie peuvent entraîner de graves vulnérabilités. Dans cet article, nous analyserons le top 10 des vulnérabilités C++ avec des exemples de code pratiques et des moyens de les prévenir. Ce contenu sera utile aussi bien aux débutants qu'aux développeurs expérimentés souhaitant renforcer la sécurité de leurs projets.
1. Débordement de tampon (Buffer Overflow)
Vulnérabilité classique survenant lors de l'écriture de données au-delà des limites d'un tableau alloué. En C++, elle est particulièrement dangereuse lors de l'utilisation de tableaux bruts et de fonctions C.
#include <iostream>#include <cstring>
void vulnerable(char* input) { char buffer[10]; strcpy(buffer, input); // DANGEREUX : pas de vérification de longueur std::cout << "Buffer: " << buffer << std::endl;}
int main() { char longInput[] = "AAAAAAAABBBBBBBBCCCCCCCC"; // 24 caractères vulnerable(longInput); return 0;}Solution : Utilisez std::string ou strncpy avec une vérification explicite de la longueur. En C++ moderne, les conteneurs STL sont préférables.
void safe(const std::string& input) { std::string buffer = input.substr(0, 9); // copie sécurisée std::cout << "Buffer: " << buffer << std::endl;}2. Fuites mémoire (Memory Leaks)
Les fuites se produisent lorsque la mémoire allouée dynamiquement n'est pas libérée. Dans les applications fonctionnant longtemps, cela entraîne un épuisement des ressources.
void leakExample() { int* data = new int[1000]; // allocation mémoire // ... travail avec les données ... // oubli de delete[] data; — fuite !}Solution : Utilisez des pointeurs intelligents (std::unique_ptr, std::shared_ptr) ou des conteneurs STL.
void safeExample() { std::unique_ptr<int[]> data = std::make_unique<int[]>(1000); // la mémoire sera libérée automatiquement à la sortie de la fonction}3. Déréférencement de pointeur nul (Null Pointer Dereference)
Tenter d'accéder à la mémoire à l'adresse nullptr provoque un comportement indéfini et conduit souvent à un plantage du programme.
void process(int* ptr) { *ptr = 42; // DANGEREUX : si ptr == nullptr}
int main() { int* p = nullptr; process(p); // plantage}Solution : Vérifiez toujours les pointeurs avant utilisation. Utilisez des références si la valeur ne peut pas être nulle.
void safeProcess(int* ptr) { if (ptr == nullptr) { throw std::invalid_argument("Pointeur nul reçu"); } *ptr = 42;}4. Condition de concurrence (Race Condition)
Se produit lorsque plusieurs threads accèdent simultanément à des données partagées sans synchronisation.
#include <thread>int counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { counter++; // opération non atomique — condition de concurrence }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter: " << counter; // résultat imprévisible}Solution : Utilisez des mutex ou des opérations atomiques.
#include <atomic>std::atomic<int> counter{0};
void safeIncrement() { for (int i = 0; i < 100000; ++i) { counter.fetch_add(1); // opération atomique }}5. Utilisation de variables non initialisées
Les variables non initialisées contiennent des "déchets", ce qui peut entraîner un comportement imprévisible.
int main() { in