Sécurité Go : 8 bonnes pratiques pour les développeurs Go
Gerred Dillon
9 février 2021
0 minutes de lectureDans ce nouvel article de notre série de fiches pratiques, nous présentons huit bonnes pratiques de sécurité Go à destination des développeurs. Le langage Go intègre de nombreuses fonctionnalités qui favorisent des pratiques de développement plus sûres — par rapport aux langages plus anciens et de bas niveau comme le C — telles que le ramasse-miettes et les pointeurs fortement typés.
Ces fonctionnalités aident les développeurs à éviter les bugs susceptibles de mener à des exploitations, en les dispensant de gérer eux-mêmes la mémoire. Toutefois, les programmeurs doivent connaître certaines bonnes pratiques de sécurité. Cette fiche, rédigée par Eric Smalling et Gerred Dillon, avec l’aide de Dan Enman (ingénieur logiciel senior chez Snyk), aborde quelques-uns des sujets les plus courants.
Téléchargez la fiche pratique ici !
Utiliser Go Modules
Analyser les dépendances à la recherche de CVE
Utiliser les packages crypto standard de Go
Utiliser html/template pour éviter les attaques XSS
Utilisation de sous-shells
Éviter unsafe et cgo
Utiliser la réflexion avec parcimonie
Réduire la surface d’attaque des conteneurs
1. Utiliser Go Modules
Le système Go Modules est le système officiel de gestion des dépendances depuis la version 1.11. Les anciens systèmes Vendor et Dep sont désormais obsolètes. Go Modules permet de figer les versions des dépendances, y compris celles des modules transitifs, et protège également contre les modifications inattendues des modules grâce à la base de données de sommes de contrôle go.sum.
Commencez par initialiser votre projet en exécutant go mod init [namespace/project-name] dans le répertoire de niveau supérieur.
Cette commande crée dans le répertoire actuel un fichier nommé go.mod, qui contient le nom de votre projet et la version de Go que vous utilisez. Si votre code source contient des imports de packages, il suffit d’exécuter go build (ou test, install, etc.) pour mettre à jour le fichier go.mod avec les modules utilisés et leurs versions. Vous pouvez également utiliser go get pour mettre à jour vos dépendances, notamment vers des versions précises ; cette commande met aussi à jour go.mod.
Exemple de fichier go.mod :
Vous remarquerez qu’un fichier nommé go.sum a également été créé. Ce fichier contient une liste des hachages de chaque module utilisé, que Go exploite pour vérifier que les mêmes binaires sont utilisés à chaque compilation. Les fichiers go.mod et go.sum doivent tous deux être ajoutés au contrôle de version avec le code de votre application.
Le tutoriel Using Go Modules, publié sur le blog officiel de Go, est une excellente ressource pour en savoir plus sur Go Modules, notamment sur la manière de figer les versions des dépendances transitives, de supprimer les dépendances inutilisées et bien plus encore.
2. Analyser les dépendances à la recherche de CVE
Comme dans la plupart des projets, le code contenu dans les modules dont dépend votre application dépasse souvent en volume celui de l’application elle-même. Ces dépendances externes constituent une source fréquente de vulnérabilités. Des outils comme Snyk, qui s’appuie sur notre vaste base de données des vulnérabilités, peuvent analyser ces graphes de dépendances à la recherche de vulnérabilités connues, suggérer des mises à niveau pour corriger les problèmes détectés et même surveiller vos projets en continu afin de vous alerter sur les nouvelles vulnérabilités découvertes.
Par exemple, il suffit d’exécuter snyk test sur une application Go pour analyser ses modules et signaler les CVE connues, ainsi que les versions corrigées vers lesquelles vous pouvez effectuer une mise à niveau. Les outils Web de Snyk peuvent également surveiller directement et en continu vos dépôts GitHub, et vous alerter des vulnérabilités découvertes, même si vous n’avez pas modifié votre code ni lancé de build CI.


3. Privilégier les packages crypto standard de Go aux packages tiers
Les packages crypto de la bibliothèque standard de Go font l’objet d’audits approfondis par des chercheurs en sécurité. Toutefois, ils ne couvrent pas tous les besoins : vous pourriez donc être tenté d’utiliser des packages tiers.
Comme pour la création de vos propres algorithmes cryptographiques, méfiez-vous fortement des bibliothèques cryptographiques tierces : elles n’ont pas forcément été auditées avec la même rigueur. Vérifiez leur provenance.
4. Utiliser html/template pour éviter les attaques XSS
Les chaînes non filtrées renvoyées à un client Web avec io.WriteString() ou le package text/template peuvent exposer vos utilisateurs aux attaques par script intersite (XSS). En effet, les balises HTML présentes dans les chaînes renvoyées sont transmises dans le flux de sortie sans encodage. De plus, si vous ne le définissez pas explicitement, l’en-tête de réponse Content-Type: plain/text risque d’être incorrect.
Utiliser le package html/template est un moyen simple d’encoder automatiquement pour le Web le contenu renvoyé, plutôt que de devoir vérifier que vous l’avez fait manuellement dans la logique de votre application. La documentation OWASP/GO-SCP propose un excellent chapitre avec des exemples détaillés sur ce sujet.
5. Utilisation de sous-shells
Dans Go, un subshell donne en quelque sorte un accès direct au shell de votre système ; son utilisation est généralement réservée aux applications de type outil en ligne de commande. Dans la mesure du possible, privilégiez toujours les solutions implémentées nativement en Go à l’aide de modules adaptés.
Si vous devez utiliser un sous-shell, veillez à assainir toutes les données provenant de sources externes susceptibles de lui être transmises, ainsi que les données renvoyées, afin que votre application ne révèle pas inutilement de détails sur le système sous-jacent. Cette précaution s’apparente à celle requise pour se protéger contre les attaques visant les modèles rendus (voir le point 4 ci-dessus) ou les injections de commandes SQL. Tenez également compte du fait qu’un appel à un processus externe dans le cadre d’une requête de l’application peut entraîner d’autres effets secondaires que vous ne pouvez pas contrôler depuis votre code Go : modifications du système de fichiers, appels à des dépendances externes ou changements de l’environnement de sécurité susceptibles de bloquer ces appels, par exemple les restrictions liées à l’exécution dans un conteneur ou à des outils comme AppArmor ou SELinux.
6. Utiliser unsafe et cgo avec prudence
Comme le C, Go prend en charge les variables de type pointeur, mais applique une sécurité de typage stricte pour protéger les développeurs contre les effets secondaires involontaires, voire malveillants. En C, vous pouvez toujours définir un pointeur void* sans type ; pour faire la même chose en Go, vous pouvez utiliser le package standard au nom évocateur unsafe afin de contourner les restrictions de sécurité de typage. La documentation Go déconseille généralement l’utilisation d’unsafe, car ce package permet un accès direct à la mémoire. Associé à des données utilisateur, cet accès peut permettre à des attaquants de compromettre la sécurité mémoire de Go.
L’utilisation de cgo suscite des inquiétudes similaires. Cette commande puissante permet d’intégrer des bibliothèques C arbitraires à votre application Go. Comme tout outil puissant, cgo doit être utilisé avec une extrême prudence : vous faites confiance à une dépendance entièrement externe, écrite dans un langage non sécurisé, pour avoir tout fait correctement. En cas de bugs ou de routines malveillantes dans ce code externe, le filet de sécurité mémoire de Go ne pourra pas vous protéger. Vous pouvez désactiver cgo en définissant simplement CGO_ENABLED=0 lors du build. C’est généralement une option sûre si vous n’en avez pas explicitement besoin, car la plupart des bibliothèques Go modernes sont écrites exclusivement en Go.
7. Réflexion
Go est un langage fortement typé : le type des variables est donc important. Il peut parfois être nécessaire d’obtenir, à l’exécution, des informations sur le type ou la valeur d’une variable. Go propose le package `reflect`, qui permet de rechercher et de manipuler le type et la valeur d’une variable de type arbitraire, par exemple pour déterminer si une variable est d’un certain type ou contient certaines propriétés ou fonctions.
La réflexion peut être utile, mais elle augmente également le risque d’erreurs de typage à l’exécution dans votre code Go. Si vous tentez de modifier une variable réfléchie d’une manière qui n’est pas autorisée (par exemple, en définissant une valeur qui ne peut pas être définie sur une structure), votre code déclenchera une panique. Il peut également être difficile de bien comprendre le flux du code, ainsi que les différentes sortes de types et de valeurs qui font l’objet de la réflexion. Enfin, lorsque vous travaillez avec des types ou des valeurs réfléchis, vous devrez parfois effectuer des assertions de type, ce qui peut compliquer le code et entraîner des erreurs à l’exécution.
La réflexion est un outil puissant, mais compte tenu du système de typage et d’interfaces de Go, son usage doit rester rare : elle peut facilement provoquer des problèmes inattendus.
8. Réduire la surface d’attaque des conteneurs
De nombreuses applications Go n’ont aucune dépendance externe et sont conçues pour s’exécuter dans des conteneurs. Il faut donc réduire le système de fichiers auquel elles ont accès en utilisant quelques techniques de création d’images. L’un des moyens les plus simples consiste à utiliser un Dockerfile multi-stage : vous compilez l’application dans une étape de build, puis utilisez une image de base scratch pour l’image de déploiement.
Voici un exemple de Dockerfile :
Si les Dockerfiles sont nouveaux pour vous, sachez qu’il s’agit d’instructions étape par étape que presque toutes les compilations d’images OCI peuvent utiliser pour créer des images. Vous trouverez leur documentation ici. Cet exemple de Dockerfile multi-stage comprend deux étapes distinctes : une étape de build et l’étape de l’image finale, utilisée à l’exécution.
Étape 1, lignes 1–12 : l’étape de build
En partant de l’image de base officielle golang:1.15, nous définissons dans cette étape quelques variables d’environnement et compilons notre application Go. À la fin de cette étape, une image temporaire est mise en cache sous l’étiquette build, à laquelle nous pourrons faire référence par la suite.
Vous vous demandez probablement à quoi servent toutes les variables d’environnement et tous les arguments transmis au build :
GOPATH=””: on vide cette variable (qui était définie dans l’image de base
golang:1.15), car elle n’est pas nécessaire avec Go Modules.CGO_ENABLED=0: désactivecgo(voir la section 6 ci-dessus).GOOS=linux: indique explicitement à Go de compiler pour le système d’exploitation Linux.GOARCH=amd66: indique explicitement à Go de compiler pour l’architectureamd64(Intel).-trimpath: supprime les informations de chemin du système de fichiers du binaire.ldflag -s: omet la table des symboles et les informations de débogage.ldflag -w: omet la table des symboles DWARF.
Ces paramètres permettent de produire un binaire aussi minimal que possible, mais certains ne conviennent peut-être pas à votre application : choisissez-les selon vos besoins.
Étape 2, lignes 13–10 : l’étape de l’image d’exécution
Dans cette étape, nous copions simplement le binaire statique et les fichiers /etc/passwd depuis l’étape build vers un système de fichiers scratch vide, puis nous définissons les droits de propriété appropriés et la commande à exécuter au démarrage du conteneur.
Pour créer cette image, exécutez simplement la commande suivante depuis le même répertoire que le Dockerfile :
Remarque : le . à la fin de la commande de build est important : il indique au système de build où trouver le Dockerfile et les autres fichiers auxquels il pourrait faire référence.
L’image obtenue contiendra exactement deux fichiers dans son système de fichiers : notre application et le fichier passwd, qui contient notre utilisateur moby (le nom de l’utilisateur importe peu ; nous voulons simplement éviter d’exécuter quoi que ce soit en tant que root dans le conteneur). Elle ne contiendra ni sh, ni ps, ni aucun autre fichier exploitable par un attaquant. Bien sûr, si vous avez besoin d’autres fichiers pour faire fonctionner votre application, vous devrez les inclure ou les monter au moment de l’exécution.
Téléchargez la fiche pratique sur la sécurité Go ici !
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.
