GitHub Actions et le problème du "pinning" : ce que révèlent 100 projets de sécurité
Adan Alvarez a analysé les 100 dépôts GitHub les plus populaires dans le domaine de la sécurité (selon l’Open Source Security Index), à l’aide d’un script personnalisé. Ce script parcourt toutes les GitHub Actions utilisées dans un dépôt, vérifie si elles sont « pinnées » (c’est-à-dire figées sur un hash SHA, et non sur un tag comme @v1
, qui peut changer avec le temps), et examine également récursivement les dépendances des Actions.
Les résultats sont sans appel :
Conclusion : L’utilisation implicite de versions comme :latest
ou @v1
représente un risque majeur en matière de sécurité, car ces références sont mutables — leur contenu peut changer sans avertissement. À l’inverse, pinner explicitement chaque action avec son hash SHA garantit l’intégrité et la prévisibilité des workflows. Cette pratique, bien que simple à mettre en œuvre, reste encore trop rare, même dans les projets à haute sensibilité sécuritaire.
Avant, j'utilisais souvent l'étiquette :latest
pour mes images de base dans les fichiers Docker, ce qui impliquait un versioning implicite.
Cette pratique semblait pratique pour s'assurer d'avoir la dernière version d'une image sans avoir à spécifier manuellement une version à chaque fois.
Cependant, cette approche présentait plusieurs inconvénients significatifs.
- Elle compromettait la reproductibilité des environnements de développement et de production. Deux builds successifs pouvaient aboutir à des environnements différents si une nouvelle version de l'image était publiée entre-temps.
- Ca introduisait une incohérence et une opacité dans le contrôle des versions utilisées, rendant difficile la détection et la correction des problèmes liés à des mises à jour incompatibles ou bug.
- Ca pouvait conduire à des problèmes de sécurité si la dernière version contenait des vulnérabilités non identifiées au moment du déploiement.
Maintenant, je spécifie explicitement le versioning de chaque image Docker en fixant une version spécifique, comme :12
, par exemple.
Cette approche a plusieurs impacts positifs:
- elle assure une reproductibilité cohérente des environnements en éliminant les variations inattendues dues à des mises à jour automatiques.
- ça facilite également le diagnostic et la résolution de problèmes en permettant à tous les membres de l'équipe de travailler avec des versions identiques des dépendances.
- ça améliore la sécurité en permettant une évaluation des vulnérabilités connues pour chaque version utilisée
L'utilisation d'outils comme Snyk ou Renovate-bot permet de simplifier la gestion des mises à jour en détectant automatique les nouvelles versions disponibles et propose des mises à jour via des MR/PR, facilitant le passage d'un versioning implicite à un versioning explicite sans coût supplémentaire.
Cela permet non seulement un meilleur contrôle sur ce qui est déployé en production mais aussi d'intégrer ces mises à jour de manière contrôlée et sécurisée.
Nous autorisions les appels HTTP des clients même s'ils ne précisaient pas de numéro de version. Par défaut la version était donc implicite, à savoir la dernière version publiée (latest).
Notre API refuse maintenant les appels d'API lorsque le numéro de version (MAJOR.MINOR) n'est pas précisé. Nos consommateurs doivent donc explicitement préciser la version. Soit via un header (Accepter: application/vnd.maboite.vMAJOR.MINOR+json), soit via le chemin (e.g. /vMAJOR.MINOR), soit via un query parameter (?v=MAJOR.MINOR).
Les dépendances du projet était en "*" (a-k-a "latest" a-k-a "utilise la dernière version publiée de cette dépendance"). D'une construction à l'autre, le projet pouvait ne plus être livrable.
La gestion des dépendances du projet est maintenant explicite, la version de chaque dépendance est explicitement spécifiée en vX.Y.Z. L'équipe utilise un fichier de lock par projet (c.f. javascript, rust, ruby, php) pour assurer la reproductibilité des constructions.
Pull d'une image de container (e.g. docker) sans préciser de tag. Cela revient à préciser le tag "latest", à savoir la dernière version publiée de l'image. Cet anti-pattern peut s'observer dans les fichiers docker-compose.yml, yaml Kubernetes ou encore pour la construction d'une image depuis un Dockerfile (FROM image).
L'équipe précise maintenant explicitement le tag de chaque image dont son projet dépend et n'emploie plus le tag "latest".
Configuration écrite en dur, cachée dans le code source du programme (e.g. identifiant d'accès à la base de données).
Configuration via des variables d'environnements requises au démarrage du programme
Voila ce qu'il se passe quand un standard (ASN.1 RFC2631) laisse passer l'implicite.
Et vous, quelles sont vos retours d'expérience concernant l'application de ce principe ?