Pour l’Agence nationale de la sécurité des systèmes d'information (ANSSI), il est alors nécessaire de définir des restrictions quant à l’utilisation du langage C afin d’identifier les différentes constructions risquées ou non portables et d’en limiter voire interdire l’utilisation. Dans cette optique, l’agence a proposé un guide qui ambitionne de « favoriser la production de logiciels plus sécurisés, plus sûrs, d’une plus grande robustesse » en plus de favoriser leur portabilité d’un système à un autre, qu’il soit de type PC ou embarqué. Précisons que dans son document, l’Agence s’est limitée aux deux standards C90 et C99 qui restent « les plus utilisés ».
Convention de développement
Avant toute chose, tout projet de développement, quel qu’il soit doit suivre une convention de développement claire, précise et documentée. Cette convention de développement doit absolument être connue de tous les développeurs et appliquée de façon systématique. Chaque développeur a ses habitudes de programmation, de mise en page du code et de nommage des variables. Cependant, lors de la production d’un logiciel, ces différentes habitudes de programmation entre développeurs aboutissent à un ensemble hétérogène de fichiers sources, dont la vérification et la maintenance sont plus difficiles.
Aussi l’ANSSI propose la règle suivante : « Des conventions de codage doivent être définies et documentées pour le logiciel à produire. Ces conventions doivent définir au minimum les points suivants : l’encodage des fichiers sources, la mise en page du code et l’indentation, les types standards à utiliser, le nommage (bibliothèques, fichiers, fonctions, types, variables...), le format de la documentation. Ces conventions doivent être appliquées par chaque développeur ».
Cette règle autour d’une convention de développement est certes évidente et le but ici n’est pas d’imposer, par exemple, un choix de nommage de variable (tel que snake-case au lieu de camel-case), mais de s’assurer qu’un choix a bien été fait au début du projet de développement et que celui-ci est clairement explicité. En fait, au début de la réalisation d’un projet informatique, l’équipe de développement devrait toujours s’accorder sur les conventions de codage à appliquer. Le but est de produire un code source cohérent. Par ailleurs, le choix de conventions judicieuses permet de réduire les erreurs de programmation
L’ANSSI a donné un exemple de conventions de codage, prenant la peine de préciser que « certains choix sont arbitraires et discutables ». Et d’indiquer que « cet exemple de conventions peut être repris ou servir de base, si aucune convention de développement n’a été définie pour le projet à produire. Différents outils ou des éditeurs avancés sont en mesure de mettre en œuvre de façon automatique certaines de ces conventions de codage » :
- Encodage des fichiers :
- Les fichiers sources sont encodés au format UTF8.
- Le caractère de retour à la ligne est le caractère « line feed » \n (retour à la ligne au format Unix)
- Longueurs maximums :
- une ligne de code ou de commentaire ne doit pas dépasser 100 caractères.
- Une ligne de documentation ne doit pas dépasser 100 caractères.
- Un fichier ne doit pas dépasser 4000 lignes (documentation et commentaires compris).
- Une fonction ne doit pas dépasser 500 lignes.
- Indentation du code :
- L’indentation du code s’effectue avec des caractères espace : un niveau d’indentation correspond à 4 caractères espaces.
- L’utilisation du caractère de tabulation comme caractère d’indentation est interdite.
- La déclaration des variables et leur initialisation doivent être alignées à l’aide d’indentations.
- Un caractère espace est laissé systématiquement entre un mot clé et la parenthèse ouvrante qui le suit.
- L’accolade d’ouverture d’un bloc est placée sur une nouvelle ligne.
- L’accolade de fermeture de bloc est également placée sur une nouvelle ligne.
- Un caractère espace est laissé avant et après chaque opérateur.
- Un caractère espace est laissé après une virgule.
- Le point-virgule indiquant la fin d’une instruction est collé au dernier opérande de l’instruction.
Compilation
Les compilateurs proposent différents niveaux d’avertissement afin d’informer le développeur de l’utilisation de constructions risquées ou de la présence d’erreurs de programmation. D’un compilateur à l’autre, le comportement par défaut n’est pas identique pour une même version du standard C utilisée par exemple. Même les avertissements émis lors de la compilation sont directement liés à la version du compilateur. Il est donc primordial de connaître exactement le compilateur utilisé, sa version, mais aussi toutes les options activées avec, dans l’idéal, une justification pour chacune d’elles. De plus, les optimisations de code faites au niveau de la compilation et de l’édition de liens doivent être utilisées en pleine conscience des impacts associés au niveau de l’exécutable généré.
L’ANSSI estime que la maîtrise des actions à opérer à la compilation est nécessaire : « le développeur doit connaître et documenter les actions associées aux options activées du compilateur y compris en terme d’optimisation de code ».
Le niveau d’avertissement activé par défaut dans les compilateurs est souvent un niveau peu élevé,signalant peu de mauvaises pratiques. Pour l’Agence, ce niveau par défaut est insuffisant et doit donc être augmenté. Cela implique que les options de compilation utilisées doivent être explicitées.
Elle émet donc la règle suivante :
« Les options utilisées pour la compilation doivent être précisément définies pour l’ensemble des sources d’un logiciel. Ces options doivent notamment fixer précisément :
- la version du standard C utilisée (par exemple C99 ou encore C90);
- le nom et la version du compilateur utilisé;
- le niveau d’avertissements (par exemple-Wextra pour GCC);
- les définitions de symboles préprocesseurs (par exemple définir NDEBUG pour une compilation en release)».
Différentes options de durcissement à la compilation existent pour prévenir ou limiter l’impact, entre autres, des attaques sur le formatage de chaînes de caractères, des dépassements de tas ou de pile, de réécriture de section ELF ou de tas, l’exploitation d’une distribution non aléatoire de l’espace d’adressage. Ces options ne sont pas une garantie absolue contre ce type d’attaques, mais permettent d’ajouter des contremesures au code par rapport à une compilation sans durcissement. Ces options de durcissement peuvent avoir un lien direct avec les niveaux d’optimisation du compilateur (comme l’option-D_FORTIFY_SOURCE pour GCC qui n’est effective qu’avec un niveau d’optimisation supérieur ou égal à 1) et peuvent être actives, en partie, par défaut pour les versions les plus récentes du compilateur.
Face à cela, l’ANSSI en a fait une règle : « L’utilisation d’options de durcissement est obligatoire que ce soit pour imposer la génération d’exécutables relocalisables, une randomization d’adresses efficace ou la protection contre le dépassement de pile entre autres ». L’Agence demande cependant d’éviter les options d’optimisation du compilateur comme -fno-strict-overflow, -fwrapv,-fno-delete-null-pointer-checks, -fno-strict-aliasing qui peuvent affecter la sécurité.
La bonne pratique consiste à utiliser des générateurs de projets pour la compilation. En effet, l’utilisation d’un générateur de projets, tels que make, Cmake ou Meson, facilite la gestion des options de compilation. Celles-ci peuvent être définies de façon globale et appliquées à tous les fichiers sources à compiler.
Déclarations
Le langage C autorise la déclaration de plusieurs variables d’un même type simultanément en séparant chaque variable par une virgule. Cela permet d’associer à un groupe de variables un type donné et de regrouper ensemble des variables dont le rôle est lié. Cependant ce type de déclaration multiple ne doit être utilisé que sur des variables simples (pas de pointeur ou de variable structurée) et de même type.
L’ANSSI recommande donc que « Seules les déclarations multiples de variables simples de même type soient autorisées ».
Pour éviter également toute erreur dans l’initialisation de variables, les initialisations couplées à une déclaration multiple sont à proscrire. En effet, en cas d’initialisation unique à la fin de la déclaration multiple, seule la dernière variable déclarée est effectivement initialisée.
D’où la règle consistant à ne pas faire de déclaration multiple de variables associée à une initialisation : « les initialisations associées (i.e.consécutives et dans une même instruction) à unedéclaration multiple sont interdites ».
Déclaration libre de variables
Depuis C99, la déclaration de variables peut se faire partout dans le code. Cette fonctionnalité semble pratique, mais un abus peut complexifier de façon notable la lecture du code et peut entraîner de possibles redéfinitions de variables.
L’ANSSI recommande alors de regrouper les déclarations de variables en début du bloc dans lequel elles sont utilisées : « pour des questions de lisibilité et pour éviter les redéfinitions, les déclarations de variables sont regroupées en début de fichier, fonction ou bloc d’instructions selon leur portée ».
Cette recommandation n’est pas directement liée au sens strict à la sécurité, mais impactant la lisibilité, portabilité et/ou maintenabilité du code, elle concerne tout type de développement. Une pratique très courante pour les compteurs de boucle est de les déclarer directement dans la boucle associée. Ce cas de déclaration « libre » est accepté, mais il faut s’assurer que la variable associée à ce compteur de boucle ne masque pas une des autres variables utilisées dans le corps de la boucle.
Par exemple, dans le code suivant, les variables sont déclarées « au fil de l’eau » et non de façon groupée et structurée. Ce genre de pratique complexifie l’identification de toutes les variables déclarées augmentant par conséquent le risque de masquage des variables :
Code C : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include<stddef.h> uint8_tglob_var;/*variable globale*/ uint8_tfonc(void){uint8_tvar1;/*variable locale*/ if(glob_var>=0) { /*...*/ } else { var1=glob_var; uint8_tvar2;/*autre variable locale déclarée au milieu d'un bloc*/ /*...*/ } uint8_tglob_var2;/*autre variable globale déclarée entre deux fonctions*/ int main(void) { uint8_tx=fonc() ; /*...*/ } |
Le bon exemple consisterait à déclarer les variables de façon groupée et structurée en début des blocs, ce qui facilite la lecture :
Code C : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include<stddef.h> uint8_tglob_var;/*variables globales déclarées ensemble*/ uint8_tglob_var2 uint8_tfonc(void) { uint8_tvar1;/*variables locales déclarées ensemble au début de la fonction*/ uint8_tvar2; if(glob_var>= 0) { /*...*/ } else{ var1=glob_var; } /*...*/ } int main(void) { uint8_tx=fonc() ; /*...*/ } |
Source : guide des règles de programmation pour le développement sécurisé de logiciels en C
Et vous ?
Que pensez-vous de cette initiative ?
Quelles sont les recommandations ou règles qui vous intéressent le plus.
Voir aussi :
Rust entre dans le top 20 de l'indice Tiobe de popularité des langages de programmation pour la première fois, C conserve la tête du classement et Java la seconde place
Le langage C dépasse Java et devient le langage le plus populaire selon l'index TIOBE et Rust se rapproche du top 20 du classement
Apple recherche des ingénieurs logiciels afin de convertir une base de code établie en C vers Rust
Le langage C ne sera-t-il jamais battu en termes de rapidité d'exécution et de faible consommation d'énergie ? Voici les résultats d'une étude sur 27 langages de programmation les plus populaires