Publié le 24/01/2024 Par Houda EL HILALI

Dans le cadre des activités de développement des logiciels, quel que soit le langage de programmation, des vulnérabilités peuvent être présentes dans le code. Ces failles affectent le niveau de sécurité des applications et peuvent être exploitées de différentes manières. Le risque principal : qu’elles mettent à genoux toute une entreprise à l’image de ce qu’il s’est passé chez Microsoft. Dans cet article, nous vous présentons les vulnérabilités qu’un développeur ou une développeuse Python doit prendre en compte durant tout le cycle de vie du développement.

un développeur en train de travailler meritis

Pour commencer, voici ci-dessous une liste non exhaustive des vulnérabilités liées non seulement à Python, mais également à d’autres langages :

  • Injection SQL
  • Injection shell
  • XML External Entities (XXE)
  • Typosquatting des paquets
  • API key leak

Ces vulnérabilités représentent des faiblesses dans une application développée qui permettraient à une personne malveillante d’altérer le fonctionnement normal de l’application ou d’accéder à des données non autorisées.

L’origine de ces failles est généralement involontaire et peut résider dans la conception du logiciel.

1.

L’injection SQL

Abrégée en SQLi, l’injection SQL est une vulnérabilité permettant à un attaquant d’utiliser un morceau de code SQL pour manipuler la base de données. C’est l’une des attaques les plus connues et menaçantes qui peut nuire à n’importe quelle base de données d’applications web ou site web utilisant SQL.

L’attaque se présente sous la forme d’un input contenant des requêtes SQL non vérifié par le code, faute de faire confiance aux entrées saisies le plus souvent via un formulaire.

un homme au téléphone qui ne comprends pas la la requette devant son ordinateur

L’exemple de code ci-dessous présente une API flask qui renvoie à une liste de films d’une certaine année. Le problème ici est qu’elle ouvre la porte à un attaquant lui permettant alors de manipuler la base de données :

@app.route(‘/movies’, methods=[‘GET’])
def fetch_movies():
year = request.args.get(‘year’)
if year is None:
return jsonify({‘error’: ‘Year parameter is missing.’}), 400 ddd

connection = sqlite3.connect(‘movies.db’)
cursor = connection.cursor()
query = « SELECT title FROM movies WHERE year = ‘%s' » % year
cursor.execute(query)
movies = [row[0] for row in cursor.fetchall()]
connection.close()

return jsonify({‘movies’: movies})

Une personne mal intentionnée pourrait profiter de la faille du code ci-dessus comme suit :

payload = « 2020 »
payload += « ‘ UNION SELECT password_hash FROM users;–« 
#Make a GET request to the vulnerable endpoint
response = requests. Get(« http://localhost:5000/movies2?year= » + payload)

Ici au lieu de donner l’année l’utilisateur envoie aussi un bout de requête SQL. Aucune vérification n’est appliquée, le bout de SQL sera ajouté à query comme si c’était un paramètre normal, alors à ce moment cursor.execute(query) la valeur de query sera SELECT title FROM movies WHERE year = ‘2022’ UNION SELECT password_hash FROM users;  et les mots de passe des utilisateurs seront renvoyés à l’attaquant comme si c’était un résultat normal.

Plusieurs entreprises ont été victimes d’attaques liées à cette faille, en témoignent les articles suivants :

Comment se protéger d’une cyberattaque via une faille Python ? (Tip 1) 

Pour se protéger de ce type d’attaque, il faudrait :

  • Valider les saisies entrées par les utilisateurs en s’assurant qu’elles sont bien en accord avec l’attendu.
  • Faire de la ségrégation de données, c’est-à-dire séparer les données sensibles du reste et accorder des accès restreints aux utilisateurs.
  • Privilégier les requêtes préparées. Les librairies clientes de toutes les grandes bases de données fournissent cette fonctionnalité. Ce type de requêtes permet de vérifier que ce qui est entré est bien les paramètres avec le type attendu. Ce qui ressemble à ceci : select columns from table where column = ? avec le « ? » indiquant le paramètre. La librairie va alors se charger d’insérer le paramètre en toute sécurité.

Appliquons cela au bout de code vulnérable présenté au-dessus :

# Function to retrieve movies from a given year
def get_movies_secure(year):
connection = sqlite3.connect(‘movies.db’)
cursor = connection.cursor()
# la ligne en dessous fixe le problème
cursor.execute(« SELECT title FROM movies WHERE year = ? », (year,))
movies = [row[0] for row in cursor.fetchall()]
connection. Close()
return movies

2.

L’injection shell

Moins connue que l’injection SQL, elle peut pourtant s’avérer plus dangereuse. Elle fonctionne sur le même principe mais avec du code shell au lieu du SQL.

Contrairement à l’injection SQL qui ne permet l’accès qu’à la base de données, l’injection shell permet l’accès au serveur avec potentiellement un compte à privilège « root ». À la clé : la possibilité de prendre le contrôle sur le serveur (changement de configuration, ouverture des ports, installation logiciel…).

Par conséquent, l’application devient vulnérable et les attaquants peuvent exploiter ces failles pour accéder aux SI, laissant les portes grandes ouvertes à l’attaquant pour lui permettre de faire ce qu’il souhaite sur vos serveurs.

une intrusion hacker

Ci-dessous, le code d’une API qui convertit un fichier média donné par l’utilisateur vers le format demandé par l’utilisateur :

from flask import Flask, request, send_file
import subprocess

app = Flask(name)

@app.route(« /convert », methods=[« POST »])
def convert():
media_file = request.files[« media_file »]
format = request.form[« format »]

#Save the media file to disk
media_file.save(f »{media_file.filename} »)

# Call ffmpeg to convert the media file to the specified format
command = f »ffmpeg -y -i {media_file.filename} converted_file.{format} »
subprocess.run(command, shell=True)

# Send the converted file as a response
return send_file(f »converted_file », as_attachment=True)

Cette API utilise le logiciel ffmpeg qui doit être installé sur le serveur.
🔍 Cependant, il y a deux failles de sécurité dans ce code. Saurez-vous les trouver ?

Path traversal 

Cette API reçoit un fichier dans le paramètre media_file. Un attaquant pourrait alors manipuler le nom du fichier envoyé, étant donné qu’il peut envoyer ce qu’il veut en paramètre de la requête. Donc, au lieu d’envoyer un simple nom de fichier, il pourrait envoyer un chemin /home/bin/malicious_file.sh.

Cette ligne media_file.save(f »{media_file.filename} ») sauvegarde le fichier à l’emplacement donné par l’utilisateur sans aucune vérification du chemin donné. L’attaquant peut enregistrer son fichier où il veut, ou même écraser des fichiers existants qui pourraient être des fichiers de code ou de configuration de l’application. 

Shell injection

L’API reçoit aussi le paramètre format. Le problème est ici :

    # Call ffmpeg to convert the media file to the specified format
    command = f »ffmpeg -y -i {media_file.filename} converted_file.{format} »
subprocess.run(command, shell=True)

 L’attaquant peut envoyer en paramètre « format » ‘avi; rm *’ Il y a bien le format mais il y a aussi une commande rm. Cette commande sera exécutée par subprocess.run(command, shell=True), l’option shell=True faisant en sorte que la fonction run instancie un shell et lance la commande dans ce shell. Ce qui rend possible l’injection. Autrement, Python aurait lancé ffmpeg dans un nouveau processus, sans passer par un shell, et le ; rm * aurait été passé comme un simple argument à ffmpeg.

 Voici un exemple de code d’attaque complet :

# Set the file and format parameters
files = {« media_file »: (« ~/.ssh/authorized_keys », open(« ../attack_key », « rb »))}
data = {« format »: « avi; rm * »}
# Send a POST request to the Flask route with the file and format parameters
response = requests. Post(url, files=files, data=data)

 Comme on peut constater dans le code, l’attaquant arrive à créer un fichier facilement où il veut dans le serveur.

Comment se protéger de l’injection shell ? (Tip 2)

illustation question meritis

Pour se protéger de ce type d’injection, il faudrait :

  • Bien valider les entrées saisies par les utilisateurs ! En utilisant par exemple des whitelist (dans l’exemple ci-dessus, une liste de formats acceptés en entrée).
  • Ne pas utiliser l’option shell=True dans l’API subprocess : sans cette option, l’appel à subprocess.run ne va pas interpréter la syntaxe shell.
  • Utiliser le module shlex, notamment la fonction quote.
  • Lancer les applications avec des utilisateurs ayant des privilèges restreints.
  • Lancer les applications dans des conteneurs.
  • Ne pas faire d’appel système avec des entrées utilisateurs.

3.

XML External Entity attack

Cette faille consiste à intégrer des données non fiables directement dans des documents XML sans validation ou échappement approprié. Les développeurs peuvent introduire cette vulnérabilité lorsqu’ils ne prennent pas suffisamment de précautions pour valider ou filtrer les données entrantes XML.

Cette vulnérabilité utilise la fonctionnalité Xml entities qui permet à un attaquant de manipuler le contenu des macros XML afin de divulguer des données sensibles ou d’injecter un code malveillant.

Xml entities est une méthode de représentation d’un objet de données dans un fichier xml. Ces entités sont au format spécifique du langage xml. Xml entities permet ainsi des entités customisées définies dans le DTD ou externes à ce dernier.

On peut voir ça comme une sorte de macro, par exemple :

<?xml version= »1.0″?>
<!DOCTYPE foo [
    <!ENTITY company « Meritis »>
    <!ENTITY xxe SYSTEM « file://etc/hostname » >
]>
<data>
    <owner>server owner : &company;</owner>
    <host>server name : &xxe;</host>
</data>

Si on lance ce code :

from lxml import etree
tree = etree.fromstring(example_xml)
for text_node in tree.xpath(« //text() »):
    print(text_node.strip())

On obtient :

  • server owner : Meritis
  • server name : perso

Utiliser la librairie lxm sur des entrées utilisateur est dangereux. Ci-dessous, le code d’une API qui fait de l’extraction de texte à partir d’un XML :

@app.route(« /parse-xml », methods=[« POST »])
def parse_xml():
   xml_data = request. Data
    try:
        tree = etree.fromstring(xml_data)
    except etree.ParseError:
        return « Invalid XML data », 400
    return « \n ».join(tree.xpath(« //text() »))

Cette API parse le XML de l’utilisateur sans aucune restriction, et le XML peut contenir des références à des entités externes comme dans l’exemple vu plus haut.

Ci-dessous, un exemple de commande qui peut exploiter l’API pour faire fuiter de la donnée : 

curl -X POST \
    -H « Content-Type: application/xml » \
    -d ‘<?xml version= »1.0″?><!DOCTYPE foo [<!ENTITY xxe SYSTEM
« file://etc/passwd » >]><data>&xxe;</data>’ \
https://api.com/parse-xml

Que faire contre l’attaque XML External Entity ? (tip 3)

Pour se protéger de cette attaque, il faudrait :

4.

Typosquatting des paquets malicieux

Le typosquatting des paquets malveillants est une méthode utilisée par les attaquants pour publier des logiciels malicieux ou des paquets compromis en exploitant des fautes de frappe courantes lors du téléchargement de paquets logiciels. Un développeur qui veut installer requests peut par exemple écrire pip install resquesrs, ce qui peut mener à des conséquences désastreuses.

Les attaquants profitent de ces erreurs de frappe en enregistrant des noms de domaine ou des packages malintentionnés sous ces noms erronés, similaires à ceux des sources légitimes. Cela leur permet d’exécuter un bout de code malicieux une fois qu’un développeur lance le pip install : c’est ce qu’on appelle une attaque de la chaîne logistique (supply chain attack). Comme certains installent les packages en root, cela transforme cette simple erreur humaine en catastrophe.

Il suffit que la librairie malicieuse soit installée sans qu’elle soit réellement appelée ou utilisée dans le code pour que le mal soit fait. Lorsqu’un paquet Python est installé, pip, le gestionnaire de paquets de Python, essaie de collecter et de traiter les métadonnées de ce paquet, telles que sa version et les dépendances dont il a besoin pour fonctionner correctement.

Ce processus se déroule automatiquement en arrière-plan lorsque pip exécute le script setup.py. D’ordinaire, ce script contient des métadonnées, mais on peut y mettre ce qu’on veut. Un attaquant peut donc ajouter du code malveillant dans le setup.py. Par exemple :

from setuptools import setup
with open(« virus.txt », « w ») as f:
    f.write(« booom ! »)


setup(
    # Application name:
    name= »webapp »,
    # Application author details:
    author= »Homer Smithson »,
   author_email= »homer@meritis.fr »,
    # Packages
    packages=[« webui »],
    description= »Example App »,
    test_suite= »tests »,
)

La détection de ce type d’attaque peut s’avérer compliquée car, même si pip scanne déjà les libraires publiées, et qu’il est possible de scanner soi-même les packages avec des outils comme GuardDog, Synk, etc., les attaquants trouvent des moyens de contourner les scans. Voilà pourquoi il y a quelque chose d’étrange avec ce code :

from setuptools import setup

with 𝙤𝗽𝙚𝙣(« attack_file.txt », « w ») as f:
    f.write(« b𝙤𝙤om ! »)

 Ici, je n’ai pas écrit open mais 𝙤𝗽𝙚𝙣. À l’œil nu, les deux se ressemblent mais le deuxième open n’utilise pas des caractères ascii. Ce qu’il faut savoir ici, c’est que l’interpréteur Python accepte ce code ! Il va convertir automatiquement le 𝙤 en o. L’interpréteur Python a juste fait ce choix de convertir le texte en code en acceptant les variantes. Si c’est sans doute utile dans certaines situations, cela rend aussi la tâche difficile pour les outils de sécurité qui recherchent des strings spécifiques dans le code.

 Ce type de faille peut se révéler très dangereuse, surtout si vous produisez du code réutilisé en dépendance par plusieurs applications. C’est notamment le cas en 2022 de Pytorch qui s’est retrouvé avec une dépendance exécutant un script récupérant les informations serveur ! En savoir plus sur cet incident sur le site de Pytorch.

 Vous trouverez sur Github un exemple contenant du code malicieux exploitant cette faille.

Comment remédier à Typosquatting des paquets ? (Tip 4)

 Pour se protéger de ce type de faille, il est essentiel d’adopter les bonnes pratiques suivantes :

  • Faire preuve de vigilance lors de la saisie d’URL ou de noms de paquets logiciels.
  • Vérifier dans le fichier requirements les noms des paquets, et pour chaque paquet, vérifier les auteurs.
  • Mettre à jour les paquets régulièrement et suivre le blog des dépendances que vous utilisez : cela vous permettra d’éviter cette faille et de corriger assez rapidement si besoin.

5.

API key leaks

Une fuite de clé API signifie la divulgation ou l’exposition involontaire d’une clé API, qui est un identifiant unique utilisé pour authentifier et autoriser l’accès à une API (Application Programming Interface).

 Cette faille est introduite par une mauvaise pratique des développeurs. En effet, quand les APIs sont utilisées dans le cadre de développement, des clés sont nécessaires pour solliciter ces différents APIs.

 Imaginez que, par oubli, vous avez poussé votre clé API shutterstock, qui est payante, vers un GIT partagé. Si une personne malveillante arrive sur votre répertoire GIT et s’empare de cette clé, votre facture sera forcément très conséquente. Pire encore : vous avez mis en clair les accès avec des permissions conséquentes à une API dans votre code partagé. Vous imaginez facilement ce qui peut arriver.

 Une clé API est considérée comme « un mot de passe ». Son usage doit donc être maîtrisé et son accès restreint. Et comme nous le rappellent nos banques : il ne faut jamais divulguer ses accès à qui que ce soit ! Celui-ci doit par conséquent être chiffré (pas d’écriture en clair), et stocker dans une variable d’environnement ou dans un coffre-fort sécurisé.
Pour savoir comment crypter et stocker vos clés proprement : https://cheatsheetseries.owasp.org/cheatsheets/Key_Management_Cheat_Sheet.html

🚩 Vous souhaitez encore plus vous perfectionner ? Découvre le webinar Hacking en direct : 15 failles de sécurité dans votre code.

Conclusion

Comme nous avons pu le voir tout au long de cet article, les failles sont dues principalement à une mauvaise attention²² de la part des développeurs et des développeuses, exploitée par des entrées utilisateurs ou à l’utilisation de codes externes. Faire des erreurs arrive à tout le monde. Chez vous, en rangeant la vaisselle, il vous arrive de casser un verre de temps en temps, n’est-ce pas ? La fiabilité humaine est un sujet maintes fois étudié (Swain & Guttman, méthode HEART, méthode MERMOS). Ainsi, casser un verre en rangeant arrive environ une fois sur mille. Cela peut être plus quand on est stressé ou fatigué, et moins quand on est formé et/ou expérimenté. Même les meilleurs font des erreurs, beaucoup moins que la moyenne, mais ils en font !

 C’est pourquoi en codant, avec Python comme avec tout autre langage, il faut être vigilant et garder à l’esprit qu’il faut :

  • Vérifier les entrées dont on n’est pas à la source ;
  • Déployer des utilisateurs avec des accès restreints pour faire tourner nos applications ;
  • Utiliser les outils de scan proposés par le langage ;
  • Et bien faire attention au code externe, que ce soit celui des dépendances (en suivant les releases et les news) ou celui que l’on récupère en ligne.

La sécurité d’une application est avant tout la responsabilité du développeur / développeuse car c’est le code développé qui peut introduire des failles dans l’application développée mais aussi sur le réseau où elle se trouve, dans les machines, etc. Son rôle ne s’arrête pas à coder une fonctionnalité opérationnelle mais plutôt une fonctionnalité sécurisée sans introduction de failles.

 Comment ? Grâce aux quelques bonnes pratiques ci-dessous :

  • La formation : on n’arrête jamais de se former dans la vie tout comme en IT, surtout en sécurité ! Il faut alors prendre connaissance des failles communes mais aussi des nouvelles publiées par les organismes dédiés comme l’OWASP (Open Web Application Security Project). Il s’agit d’une fondation qui publie sur le web un ensemble d’informations concernant les failles de sécurité. Mais la formation ne consiste pas uniquement à se former mais aussi à former. N’hésitez pas à créer des ateliers sur le sujet au sein de votre entreprise, et à réaliser des études de cas tout en encourageant votre équipe à faire de la veille sur le sujet.
  • L’automatisation : il ne faut surtout pas hésiter à automatiser car cela ne nous fait pas seulement gagner du temps mais cela évite les erreurs humaines qui peuvent être sources de failles. En Python, on trouve des outils open source tels bandit : ce linter de sécurité cherche automatiquement les failles les plus communes dans le code.
  • La vérification : on ne peut pas tout vérifier mais on peut mettre en place des systèmes, des bonnes pratiques et des chartes qui nous permettront d’améliorer notre sécurité. Le code review en est un bon exemple : il ne permet pas uniquement la vérification de la qualité de code mais aussi celle de ses vulnérabilités et de ses bugs.
  • Et attention de bien prendre en compte notre état mental – nous ne sommes qu’humains après tout – car il affecte directement notre code. Combien de fois sommes-nous restés coincés devant un bug alors qu’une fois que nous avons changé de sujet ou pris une pause, nous avons immédiatement trouvé son origine. C’est pourquoi il ne faut jamais hésiter à prendre du recul mais aussi à se relire.

Pas encore de commentaires

Publier un commentaire

Auteur

Houda EL HILALI

Houda, MOE chez Meritis, diplômée de l'Université de Technologie de Belfort-Montbéliard en 2018, apporte sa passion pour l'informatique à chaque projet. Son dévouement envers l'excellence technique et son esprit novateur enrichissent constamment l'équipe, faisant d'elle un élément clé de notre réussite collective.

Découvrir ses autres articles

Pas d'autres articles publiés par cet auteur pour le moment