Skip to main content

Injection de commandes en Python : exemples et prévention

Écrit par
Headshot of Rubaiat Hossain

Rubaiat Hossain

feature python command injection

21 décembre 2023

0 minutes de lecture

Malgré la réputation de simplicité et de polyvalence de Python, sécuriser les programmes Python peut s’avérer difficile si vous ou les autres membres de votre équipe négligez les bonnes pratiques de sécurité pendant le développement.

En outre, vous utiliserez probablement des bibliothèques ou d’autres projets open source pour développer une application Python. Ces ressources peuvent toutefois introduire des problèmes de sécurité supplémentaires et exposer votre programme à des attaques telles que l’injection de commandes. Cette faille de sécurité critique peut avoir des conséquences désastreuses pour vos applications Python. L’injection de commandes permet d’exécuter des commandes sans autorisation dans vos applications, ce qui peut entraîner des fuites de données, la compromission de systèmes et d’autres activités malveillantes.

Dans cet article, vous découvrirez tout ce qu’il faut savoir sur l’injection de commandes, notamment comment cette vulnérabilité peut apparaître dans vos programmes. Vous découvrirez également les bonnes pratiques de sécurité courantes pour protéger vos applications Python contre les attaques par injection de commandes.

Qu’est-ce que l’injection de commandes ?

Une injection de commandes se produit lorsqu’un attaquant peut exécuter des commandes système arbitraires en les injectant dans un programme vulnérable. Cette vulnérabilité apparaît généralement lorsqu’une application transmet des données utilisateur non sécurisées (par exemple, des formulaires, des cookies et des en-têtes HTTP) à un shell système.

Plusieurs situations peuvent entraîner des vulnérabilités d’injection de commandes, notamment la transmission de données utilisateur non assainies à des commandes système. Les attaquants peuvent injecter des arguments malveillants lorsqu’une application transmet directement des données saisies par l’utilisateur à une ligne de commande. C’est par exemple le cas de la vulnérabilité d’injection de commandes présente dans certaines versions de MLflow, une plateforme de gestion du cycle de vie du machine learning. Elle est due à des contrôles de validité insuffisants dans la méthode predict() du fichier backend.py.

En outre, les applications peuvent construire dynamiquement des chaînes de commandes à partir de différents paramètres, y compris des données utilisateur. Ces commandes peuvent être exploitées si leur construction ne fait pas l’objet de vérifications et de validations adéquates.

Malheureusement, il est facile de mal interpréter les implications des commandes système. Une mauvaise utilisation ou configuration de ces commandes peut facilement introduire des vulnérabilités d’injection de commandes. Par exemple, les versions concernées de PaddlePaddle, une plateforme d’apprentissage profond distribué parallèle, sont vulnérables à l’injection de commandes dans fs.py via la méthode os.system.

Les conséquences d’une injection de commandes sont graves. Les attaquants peuvent obtenir un accès non autorisé, exfiltrer des données sensibles ou corrompre le système. De telles violations peuvent entraîner des pertes financières, nuire à la réputation et avoir des conséquences juridiques pour les organisations, sans compter les préjudices considérables pour les utilisateurs dont les informations sensibles pourraient être volées.

Comprendre et corriger les injections de commandes en Python

L’injection de commandes reste l’une des menaces les plus graves pour la sécurité des applications. Pour protéger les applications Python contre ce type d’attaque, il faut comprendre en profondeur ces vulnérabilités et prendre des mesures proactives pour contrer les exploits potentiels.

Vulnérabilités courantes à l’origine d’injections de commandes

La section suivante présente certaines vulnérabilités courantes à l’origine d’injections de commandes en Python.

Exploitation des données contrôlées par l’utilisateur

Les données contrôlées par l’utilisateur sont des données ou des valeurs fournies directement par celui-ci, comme les données de formulaire, les paramètres d’URL ou les entrées de la ligne de commande. Lorsqu’une application traite ces données sans les valider correctement et les utilise directement pour exécuter des commandes système, elle devient vulnérable aux attaques par injection de commandes.

Prenons cet exemple en Python :

# Vulnerability: Directly using user-controlled input to execute a command.
command = input("Enter a command to execute: ")
os.system(command)

Ce code invite l’utilisateur à saisir une commande à l’aide de la méthode input. Cette donnée contrôlée par l’utilisateur est ensuite transmise directement à la méthode os.system pour exécution, sans aucune validation ni vérification. Autrement dit, les données saisies sont immédiatement traitées et exécutées par l’interpréteur de commandes du système :

Terminal Ubuntu montrant un test d’injection de commande Python avec une commande malveillante et l’affichage du contenu d’un répertoire

Dans cet exemple simple, des commandes shell de base servent à tester le code. Les attaquants utiliseront probablement des commandes dangereuses pouvant entraîner une élévation des privilèges, une perte de données et d’autres conséquences indésirables.

Utilisation non sécurisée des commandes système et du module subprocess

Les commandes système et le module subprocess de Python permettent d’exécuter des commandes shell et offrent ainsi des moyens puissants d’interagir avec le système d’exploitation sous-jacent. Toutefois, une utilisation non sécurisée peut introduire des vulnérabilités d’injection de commandes, en particulier lorsque ces fonctionnalités sont combinées à des données contrôlées par l’utilisateur.

Pour illustrer cela, examinons cette vulnérabilité :

# Vulnerability: Using subprocess with shell=True and unsanitized user input.
command = input("Enter a command for subprocess: ")
subprocess.run(command, shell=True)

Ici, les données utilisateur sont exécutées directement à l’aide de la méthode subprocess.run, avec l’option shell=True activée. L’argument shell=True signifie que la commande est exécutée par le shell système, ce qui permet d’enchaîner des commandes et d’effectuer d’autres opérations potentiellement malveillantes :

Terminal Ubuntu montrant une injection de commande dans Python via subprocess, en exécutant les commandes echo et ls.

En ne validant ni en n’assainissant les données utilisateur, le script s’expose à des attaques par injection de commandes et confie de fait le contrôle du shell système à des attaquants potentiels.

Risques liés à la construction dynamique de commandes

Il n’est pas rare que des programmes Python construisent et exécutent dynamiquement des commandes système, notamment lorsqu’il faut intégrer des données variables, comme des entrées utilisateur. Toutefois, en l’absence de mesures de sécurité adéquates, cette construction dynamique ouvre la voie aux vulnérabilités d’injection de commandes.

Examinons l’extrait de code Python suivant :

# Vulnerability: Dynamically constructing commands without sanity checks.
user_input = input("Enter a parameter: ")
command = f"echo Printing {user_input}"
os.system(command)

Ce code demande un paramètre à l’utilisateur, puis construit une chaîne de commande qui intègre directement cette donnée. La commande est censée afficher la saisie de l’utilisateur. Toutefois, comme cette donnée n’est ni validée ni assainie avant d’être intégrée à la commande, le code est vulnérable à une injection de commandes :

Terminal Ubuntu affichant une démonstration Python qui reçoit « Hello; ls -l » et affiche « Hello », suivi du contenu d’un répertoire.

Un attaquant peut exploiter cette vulnérabilité en saisissant une charge utile contenant des séparateurs de commandes ou des opérateurs de contrôle. Par exemple, un utilisateur pourrait saisir Hello; ls -l : le système afficherait d’abord "Hello", puis listerait le contenu du répertoire courant, le point-virgule servant de séparateur de commandes. Cet exemple est simple, mais les attaquants peuvent saisir des commandes plus malveillantes, comme rm -rf / ou cat /etc/shadow.

Utilisation non sécurisée de eval()

La fonction Python eval() évalue dynamiquement une chaîne en tant qu’expression Python et renvoie le résultat. Bien qu’elle soit puissante, elle peut aussi s’avérer dangereuse. Considérez l’extrait de code Python suivant :

# Vulnerability: Insecure use of the eval() method.
user_input = input("Enter a Python expression to evaluate: ")
try:
    result = eval(user_input)
    print(f"Result:\n{result}")
except Exception as e:
    print(f"Error:\n{e}")

L’utilisation de eval() dans ce code est dangereuse, car cette fonction évalue non seulement des expressions mathématiques ou des expressions Python simples, mais aussi n’importe quel code Python. Le risque est encore plus grand lorsque cette fonction est associée à la capacité de Python à interagir avec le système sous-jacent à l’aide de modules comme os.

Par exemple, un attaquant peut saisir ce qui suit :

__import__('os').popen('ls').read()

L’exemple de saisie ci-dessus importe le module os, puis exécute la commande ls sur les systèmes Unix, afin de lister le contenu du répertoire. Il s’agit d’une démonstration simple, mais des acteurs malveillants peuvent saisir des commandes plus dangereuses pour manipuler des fichiers, exfiltrer des données ou même obtenir un accès non autorisé au système :

Terminal exécutant demo.py avec une expression Python utilisant os.popen('ls').read() et affichant le contenu du répertoire.

Atténuer les vulnérabilités d’injection de commandes

Compte tenu des conséquences potentiellement graves des injections de commandes pour vos applications Python, il est essentiel de traiter efficacement ce problème. Heureusement, vous pouvez prévenir de nombreuses vulnérabilités de ce type dès les premières étapes du cycle de développement logiciel (SDLC) en appliquant les recommandations suivantes.

Valider et assainir correctement les données d’entrée

Ne faites jamais confiance aux données provenant des utilisateurs ou de sources externes. Validez-les toujours en fonction des formats attendus et assainissez-les :

import re

def sanitize_input(user_input):
    # A simple example: Allow only alphanumeric characters
    return re.sub(r'[^a-zA-Z0-9]', '', user_input)

Notez que l’exemple ci-dessus supprime uniquement les caractères non alphanumériques des données d’entrée. Cela empêche les attaquants d’enchaîner des commandes, mais une véritable application nécessitera probablement un assainissement beaucoup plus strict, par exemple en n’autorisant que certaines réponses connues.

Utiliser des requêtes paramétrées et des instructions préparées

Une requête paramétrée permet de structurer une commande en définissant d’abord celle-ci et sa structure, puis en fournissant séparément les paramètres à insérer. Ainsi, la commande et les données ne sont jamais concaténées sous forme de chaînes. Elles restent distinctes, et le système traite les données uniquement comme telles, et non comme du code exécutable.

Une instruction préparée va encore plus loin. La structure de la commande est d’abord préparée, puis il suffit de transmettre les paramètres qui remplaceront les espaces réservés. Par exemple, le module subprocess de Python permet d’exécuter des commandes en séparant celles-ci de leurs arguments, comme avec les requêtes paramétrées :

import subprocess

command = "ls"
directory = input("Which directory to list? ")
subprocess.run([command, directory])

Utiliser shell=True avec prudence

Lorsque vous utilisez le module subprocess de Python, évitez l’argument shell=True, en particulier avec des données utilisateur. Lorsque vous exécutez une commande avec shell=True, le shell système interprète toute chaîne transmise. Si cette chaîne contient des données utilisateur ou est construite à partir de plusieurs sources sans assainissement adéquat, un attaquant peut y injecter des commandes malveillantes :

command = input("Enter the directory to list: ")
subprocess.run(f"ls {command}", shell=True)

C’est la recette idéale pour une catastrophe : accès non autorisé au système, vol de données, installation de logiciels malveillants, élévation des privilèges et exécution de commandes arbitraires. Évitez d’utiliser shell=True ou, si vous n’avez pas le choix, assainissez d’abord les données d’entrée.

Éviter de transmettre des données utilisateur aux méthodes os

Transmettre directement des données utilisateur aux méthodes os, comme os.spawn ou d’autres méthodes similaires, expose votre application Python aux injections de commandes. Utilisez plutôt d’autres API Python, si elles existent pour votre cas d’usage. Si vous devez utiliser une méthode os, faites-le sans données utilisateur. Si vous ne pouvez pas vous en passer, assainissez toutes les données et utilisez des techniques d’encodage adaptées au contexte.

Limiter les privilèges et mettre en place des contrôles d’accès robustes

Exécutez toujours votre application avec le moins de privilèges possible. Si un attaquant exploite une vulnérabilité, cela limitera les dommages qu’il pourra causer. Envisagez également d’isoler votre application à l’aide d’outils ou de techniques tels que chroot, les conteneurs ou les espaces de noms utilisateur.

Conventions de codage sécurisé en Python

Les sections suivantes présentent des conventions de codage sécurisé qui, une fois mises en œuvre, peuvent contribuer à protéger vos applications Python contre les vulnérabilités d’injection de commandes.

Valider et assainir correctement les données d’entrée

La validation et l’assainissement des données d’entrée sont essentiels pour protéger vos applications Python contre les injections de commandes. En validant rigoureusement et en assainissant correctement les données utilisateur, vous réduisez efficacement la surface d’attaque, empêchez l’exécution de commandes non autorisées et améliorez la sécurité globale de vos logiciels.

Examinons le code suivant :

def get_username():
    username = input("Enter your username: ")
    if len(username) < 3 or not username.isalnum():
        raise ValueError("Invalid username!")
    return username

Cette fonction vérifie que les données d’entrée respectent une longueur définie et ne contiennent que des caractères alphanumériques avant leur traitement. Notez que, dans une application réelle, des règles de validation plus strictes que celles de ce simple exemple seront probablement nécessaires.

Utiliser avec précaution le module subprocess et les fonctions similaires

Le module subprocess peut lancer de nouveaux processus, se connecter à leurs flux d’entrée, de sortie et d’erreur, et récupérer leurs codes de retour. Il est donc essentiel de l’utiliser de manière sécurisée.

Évitez d’utiliser shell=True avec le module subprocess, sauf en cas de nécessité, car cela peut exécuter des commandes dans un shell et entraîner des attaques par injection de commandes :

# Avoid
subprocess.run("ls -l", shell=True)

# Instead Use
subprocess.run(["ls", "-l"])

Traiter les données utilisateur et les commandes externes en toute sécurité

Ne concaténez et n’interpolez jamais directement les données utilisateur dans vos chaînes de commandes. Utilisez toujours des listes d’arguments :

user_input = input("Enter a directory name: ")

# Avoid
subprocess.run(f"ls {user_input}", shell=True)

# Prefer
subprocess.run(["ls", user_input])

Utiliser des outils SAST et SCA pour détecter et corriger les problèmes de sécurité

Les outils de test de sécurité des applications statique (SAST) et d’analyse de la composition logicielle (SCA) peuvent vous aider à analyser le code que vous écrivez ainsi que les composants open source utilisés par vos logiciels, afin de détecter les vulnérabilités tôt dans le cycle de développement.

La plateforme Snyk facilite au maximum la détection et la correction des problèmes de sécurité grâce à notre approche hybride de l’IA, qui repère et corrige les vulnérabilités dans votre code et vous fournit en temps réel des retours sur les risques de sécurité potentiels ainsi que des conseils de remédiation. En utilisant des outils SAST et SCA comme Snyk (qui propose une offre gratuite à vie), vous surveillez en continu les problèmes de sécurité dans votre base de code et les corrigez tôt, avant leur mise en production. Nous pouvons ainsi vous aider à prévenir les risques suivants :

  • L’exposition à des packages malveillants susceptibles de compromettre votre système ou vos données

  • Les attaques de la chaîne d’approvisionnement, qui surviennent lorsqu’un composant de confiance est compromis et affecte tous les logiciels qui en dépendent

  • Les dépendances obsolètes susceptibles de contenir des vulnérabilités non corrigées

En intégrant des outils SAST et SCA comme Snyk à votre processus de développement, vous ne vous contentez pas de créer des logiciels fonctionnels : vous les protégez également contre un paysage de cybermenaces en constante évolution.

Bonnes pratiques pour développer des applications Python sécurisées

Dans cette section, vous découvrirez des habitudes de programmation et des workflows de haut niveau que vous pouvez adopter pour favoriser un développement Python sécurisé au sein de vos équipes.

Effectuez régulièrement des revues de code et des audits de sécurité

Des revues de code régulières, comprenant un examen approfondi de la validation des entrées, des mécanismes d’authentification et de la gestion des données sensibles, peuvent aider à détecter les vulnérabilités potentielles tôt dans le cycle de développement. En faisant participer des membres de l’équipe spécialisés en sécurité, vous pouvez repérer des problèmes qui auraient pu passer inaperçus lors du développement initial, tels que des pratiques de codage non sécurisées, une mauvaise configuration des paramètres et des entrées non assainies.

Des audits de sécurité périodiques sont essentiels pour garantir que votre base de code reste robuste face à l’évolution des menaces. Vous pouvez utiliser des outils comme OpenVAS, ZAP et Metasploit pour tester les vulnérabilités de votre programme.

Maintenez vos logiciels et bibliothèques à jour

Les logiciels et bibliothèques obsolètes contiennent souvent des vulnérabilités de sécurité connues. Vous devez mettre régulièrement à jour votre interpréteur Python, vos frameworks et vos bibliothèques tierces vers leurs dernières versions. Utilisez des gestionnaires de packages comme pip et des fichiers de configuration comme requirements.txt pour gérer et mettre à jour efficacement vos dépendances.

Tirez parti des outils et frameworks de sécurité

Vous pouvez utiliser des outils et frameworks de sécurité conçus pour Python afin d’automatiser la détection des vulnérabilités. Des outils comme Snyk peuvent vous aider à repérer les problèmes au niveau du code, les erreurs de configuration de sécurité et les vulnérabilités dans les packages tiers.

Détectez et corrigez les vulnérabilités Python

Sécurisez vos applications grâce à l’analyse des vulnérabilités de Snyk et à ses conseils de correction. 

Aucune carte bancaire requise.

Ou inscrivez-vous avec Azure AD Docker ID Bitbucket

En utilisant Snyk, vous acceptez de respecter nos politiques, notamment nos Conditions d’utilisation et notre Politique de confidentialité.

Encouragez une culture axée sur la sécurité

Il est essentiel de développer une culture de sensibilisation à la sécurité parmi les développeurs. Proposez des formations, des ateliers et des ressources pour les aider à mieux maîtriser les enjeux de sécurité. Lorsque les développeurs accordent la priorité à la sécurité dès le départ, la qualité globale des logiciels s’améliore et les risques de vulnérabilités diminuent.

Pour en savoir plus sur la création d’une culture d’entreprise axée sur la sécurité, consultez le podcast Secure Developer. Il propose de nombreux entretiens avec des responsables de la sécurité et explore en détail la manière dont les entreprises conçoivent leurs programmes de développement.

Les extensions d’IDE pour détecter les vulnérabilités en temps réel sont des outils précieux pour repérer le code vulnérable au moment où vous l’écrivez. Elles s’intègrent directement à votre environnement de développement et signalent les problèmes de sécurité potentiels en temps réel :

Visual Studio Code affiche du code Python avec des avertissements Snyk concernant une injection de commandes due à des entrées utilisateur non assainies.

En détectant les vulnérabilités pendant le développement, vous gagnez du temps et réduisez les efforts qui auraient autrement été consacrés plus tard à leur correction, une étape alors bien plus coûteuse.

Conclusion 

L’injection de commandes est un problème grave qui touche de nombreuses applications Python. Ce guide vous a montré comment cette vulnérabilité peut se manifester et quels sont les risques associés. En reconnaissant les modèles de code vulnérables, vous pouvez mieux comprendre les menaces potentielles et les contre-mesures à mettre en place.

Vous avez également découvert des pratiques de codage sécurisées, comme la validation des entrées et l’utilisation prudente des modules système intégrés qui servent à exécuter des commandes. En mettant en œuvre ces stratégies, vous pouvez renforcer la sécurité de vos applications Python, déjouer les attaques potentielles et contribuer à la résilience globale de vos logiciels.

Pour renforcer davantage votre processus de développement, pensez à utiliser des outils comme Snyk. Les extensions IDE de Snyk vous fournissent des retours de sécurité en temps réelk : elles analysent votre code pendant que vous l’écrivez et vous suggèrent des corrections immédiates. C’est comme si un expert en sécurité veillait sur vous, pour vous aider à écrire dès le départ du code sûr et résilient.

Sécurisez vos applications Python dès maintenant

Détectez et corrigez gratuitement les vulnérabilités Python avec Snyk.

Aucune carte de crédit requise.

Ou inscrivez-vous avec Azure AD Docker ID Bitbucket

En utilisant Snyk, vous acceptez de respecter nos politiques, notamment nos Conditions d’utilisation et notre Politique de confidentialité.