Checklist de sécurité pour les développeurs de logiciels

La sécurité doit être considérée comme faisant partie du système depuis le tout début, et non pas ajoutée comme une « couche » à la fin. Cette dernière solution implique un code non sécurisé (patches délicats au lieu de solutions soignées), et peut limiter la fonctionnalité et couter beaucoup plus (en temps/argent).

Voici quelques conseils et astuces pour les différentes phases de développement d'un logiciel. Cette liste n’est pas exhaustive et ne contient certainement pas toutes les recommandations que les développeurs devraient suivre. Elle inclut seulement les plus importantes – celles qui valent la peine d’être gardées à l’esprit lors du design et du développement de tout type de logiciel dans n’importe quel langage de programmation!

Général

  • Pensez aux implications concernant la sécurité que votre code pose;
  • Lisez et suivez les « guidelines » de votre langage de programmation et du type de logiciel;
  • Réutilisez du code de confiance (librairies, modules, etc.);
  • Ecrivez un code de bonne qualité, lisible et maintenable (un mauvais code ne sera jamais sûr).

Architecture

  • Modularité: Divisez le programme en parties semi-indépendantes (petites, interfaces bien définies pour chaque module/fonction);
  • Isolement: Chaque partie doit fonctionner correctement même si les autres ne fonctionnent plus (retournent des mauvais résultats, envoient des requêtes contenant des arguments invalides);
  • Défense en profondeur: Etablissez des couches multiples de défense à la place de faire confiance à un seul mécanisme de protection. Par exemple : vérifiez et validez les données entrées par l’utilisateur au point d’entrée, et vérifiez ensuite de nouveau toutes les valeurs qui sont données aux parties sensibles du code (comme le traitement des fichiers, etc.);
  • Simplicité: Il est plus probable que les solutions complexes ne soient pas sures.

Design

  • Faites que les parties sensibles-de sécurité de votre code soient petites;
  • Principe du privilège minimal: Ne requerrez pas plus de privilèges que ceux dont vous avez besoin. Par exemple : lancez votre code en tant qu’utilisateur et avec le moins de privilèges possible (ne le lancez pas en tant que « root » ou avec « SUID flag »). Assurez-vous que le compte sur lequel vous lancez votre code a seulement l’accès au fichier et exécute les privilèges dont votre code a vraiment besoin. Ne vous connectez pas à une base de données avec les privilèges administrateur depuis votre logiciel;
  • Choisissez les configurations par défaut de manière sure: Par exemple, un mot de passe aléatoire que les utilisateurs vont probablement changer plutôt qu’un mot de passe par défaut standard que beaucoup d’utilisateurs ne vont pas changer;
  • Refusez par défaut: Par exemple, lorsque vous vérifiez et validez les données entrées par un utilisateur, acceptez seulement les caractères que vous vous attendez à recevoir plutôt que d’essayer de bloquer les caractères « mauvais » connus;
  • Limitez la consommation des ressources: Afin de limiter la probabilité ou l’impact d’une attaque par déni de service;
  • "Plantez" de manière sécurisée: Par exemple, s’il y a une « runtime error » lors de la vérification des droits d’accès de l’utilisateur, assumez que celui-ci ne les possède pas;
  • Dans les applications distribuées ou Web, ne faites pas confiance au client: ne vous attendez pas à ce qu’il vérifie et valide les données entrées par l’utilisateur, effectuez des vérifications de sécurité ou authentifiez les utilisateurs – cela doit être refait du coté du serveur; rappelez-vous que les champs en-tête des réponses HTTP (cookies, « user-agent », « referrer », etc.) et les valeurs « string » des requêtes HTTP (champs cachés ou liens explicites) peuvent être contrefaits/manipulés;
  • Cryptographie: Utilisez des algorithmes, des protocoles et des produits publics et de confiance. N’inventez pas vos propres algorithmes ou protocoles de cryptographie, et n’implémentez pas ceux qui existent déjà – réutilisez du code de confiance.

Codage

  • N’ayez pas confiance dans les données entrées: Le fait que les données proviennent d’utilisateurs potentiellement malveillants est la raison la plus courante des incidents liés à la sécurité (« buffer overflow », « SQL injection », « Cross Site Scripting » (XSS), « code inside data », etc.). Les données entrées incluent des arguments de ligne de commande, des fichiers de configuration (si accessibles par des utilisateurs non-dignes de confiance), des variables d’environnement, des cookies et arguments POST/GET, etc.;
  • Vérifiez et validez toutes les données entrées: Considérez toutes les données entrées comme étant dangereuses tant que vous n’avez pas prouvé qu’elles sont valides, refusez par défaut si vous n’êtes pas sûr, vérifiez et validez à différents niveaux, par exemple au point d’entrée des données et avant d’utiliser réellement ces données;
  • Ne faites aucune supposition sur l’environnement: Assurez-vous que votre code résiste aux PATH, CLASSPATH modifiés/malveillants et aux autres variables d’environnement, répertoire courant, variable @INC Perl, umask, signaux, « open file descriptors », etc.;
  • Faites attention aux conditions: Est-ce que votre code peut tourner en parallèle? Que se passe-t-il si quelqu’un exécute deux instances de votre programme en même temps ou change l’environnement au milieu de l’exécution?
  • Gérez les erreurs et les exceptions: N’assumez pas que tout va fonctionner (en particulier les opérations fichiers, les appels aux systèmes et réseaux), « catchez » les exceptions, verifiez les résultats du code, n’affichez pas les messages d’erreur internes, les requêtes SQL échouées, les « stack trace », etc.;
  • “Plantez” gracieusement: Si une erreur non-expectée se produit dont vous n’arrivez pas à récupérer, alors enregistrez les détails, alertez l’administrateur, nettoyez le système (supprimez les fichiers temporaires, videz la mémoire) et informez l’utilisateur;
  • Protégez les mots de passe et les informations secrètes: Ne codez pas les mots de passe “en dur” (ils sont alors difficiles à changer et facile à divulguer), utilisez des fichiers externes à la place (si possible chiffrés) ou des identifiants déjà existants (comme des certificats), ou demandez simplement un mot de passe à l’utilisateur;
  • Soyez prudent lors du traitement de fichiers: Si vous voulez en créer un, signalez une erreur s’il existe déjà, lorsque vous en créez un, établissez les permissions du fichier; si vous ouvrez un fichier ou lisez des données, ne demandez pas le « droit d’écrire » (« write access »); vérifiez si le fichier que vous ouvrez n’est pas un lien avec la fonction lstat() (avant et après l’ouverture du fichier); utilisez les « pathnames » absolus (pour les commandes et les fichiers); soyez extrêmement prudent quand le nom du fichier (ou une partie) provient d’un utilisateur;
  • Fichiers temporaires: Evitez l’attaque symbolique « du lien » (quelqu’un devine le nom de votre fichier temporaire, et crée un lien depuis là vers un autre fichier, par exemple /bin/bash, que votre programme écrase). Les fichiers temporaires doivent avoir des noms uniques difficiles à deviner ! (Utilisez tmpfile() for C/C++, mktemp shell command, etc. );
  • Soyez prudent avec les appels à Shell, les fonctions « eval », etc.: De telles fonctions évaluent l’argument « string » comme du code et l’interprètent, ou l’exécutent sur le Shell. Si un attaquant réussit à injecter des données malveillantes à cet argument, vous exécutez son code.

Après l’implémentation

  • Révisez votre code et laisser d’autres le réviser aussi;
  • Lorsqu’un bug (de sécurité) est trouvé, cherchez les bugs similaires;
  • Utilisez les outils correspondants à votre langage de programmation: « bound checkers », testeurs de mémoire (« memory testers »), chercheurs de bug (« bug finders »), etc.;
  • Activez les avertissements de compilation/interprétation et lisez-les (perl -w,  gcc -Wall);
  • Désactivez les informations de « debugging » (strip command, javac -g:none, etc.).