Chapitre 19: Sécurité
L’internet peut être un endroit effrayant.
Ces temps-ci, des gaffes de sécurité de haut vol semble apparaître quotidiennement. Nous avons vu des viruses se diffuser à une vitesse impressionante, une foule d’ordinateurs compromis utiliser comme des armes, une course sans fin contre les spammeurs, et énormément de rapports d’identités volées depuis des sites web piratés.
En tant que développeurs web, nous devons faire tout ce que nous pouvons pour combattre ces forces obscures. Chaque développeur web doit traiter la sécurité comme un aspect fondamentale de la programmation web. Malheureusement, il apparaît difficile d’implémenter la sécurité - les attaquants ont seulement besoin de trouver une unique faille, alors que les défenseurs doivent protéger le tout.
Django tente de mitiger cette difficulté. Il est conçu pour vous protéger automatiquement des erreurs de sécurité courantes que font les développeurs web débutant (et même les développeurs expérimentés). Ceci dit, il est important de comprendre la nature de ces problèmes, de la manière dont Django vous protège, et - plus important - les étapes que vous pouvez suivre pour sécuriser votre code.
Pour commencer, voici un avertissement important: nous ne cherchons pas à présenter un guide exhaustif de tous les exploits de sécurité web connus, nous n’essayons donc pas d’expliquer chaque faille de façon global. Au lieu de cela, nous donnerons un bref synopsis des problèmes de sécurité tels qu’ils s’appliquent à Django.
Le thème de la sécurité du web
Si vous ne deviez retenir qu’une seule chose de ce chapitre, ce serait celle-ci:
> Ne jamais - sous aucune circonstance - faire confiance au données issues du navigateur.
Vous ne savez jamais qui est de l’autre côté de cette connexion HTTP. Il peut s’agir de l’un de vos utilisateurs, mais il peut tout aussi bien s’agir d’un infâme cracker à la recherche d’une ouverture.
Toute donnée (et quelque soit sont type) provenant du navigateur doit être traitée avec une dose slavatrice de paranoïa. Ceci inclus à la fois les données «sur la bande» (c’est à dire, soumises par des formulaires web) ou «en dehors de la bande» (c’est à dire les en-têtes HTTP, les cookies ainsi que les autres informations de requête). Il est trivial de simuler les métadonnées de requête que le navigateur ajoute en général automatiquement.
Chacune des vulnérabilités abordées dans ce chapitre découle directement de la confiance des données qui arrivent et qui ne sont pas nettoyées avant usage. Vous devriez généraliser cette pratique consistant à continuellement vous demander «d’où proviennent ces données ?».
Injection SQL
L’injection SQL est un exploit courant dans lequel un attaquant altère les paramètres d’une page web (tels que les données GET/POST ou les URLs) pour insérer des extraits de SQL arbitraires qu’une application web naïve exécutera directement dans sa base de données. C’est probablement la faille existante la plus dangereuse - et malheureusement, l’une des plus courantes.
Cette vulnérabilité surgit principalement lors de la construction du SQL - à la main - depuis l’entrée de l’utilisateur. Par exemple, imaginons l’écriture d’une fonction qui construise une liste de contact depuis une page de recherche de contact. Pour empécher les spammeurs de lire chaque courrier de votre système, nous forcerons l’utilisateur à entrer son nom d’utilisateur avant de lui fournir son adresse de courrier:
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = '%s';" %
username
# execute the SQL here...
Note
Dans cet exemple, et tous les exemples «ne faites pas cela» similaires qui suivent, nous avons délibérement laissez de côté la majorité du code nécessaire au fonctionnement réel de ces fonctions. Nous ne voulons pas que ce code fonctionne si quelqu’un venait à l’utiliser accientellement en dehors de son contexte.
Même s’il ne paraît pas dangereux à première vue, il l’est vraiment.
Tout d’abord, nous tentons de protéger laffichage de l’intégralité de notre liste de courriers en la faisant échouer grâce à une requête intelligement construite. Pensez à ce qui arriverais si un attaquant saisit "' OR 'a'='a" dans l’interface de saisie. Dans ce cas, la requête que la chaîne d’interpolation va construire sera:
SELECT * FROM user_contacts WHERE username = '' OR 'a' = 'a';
Puisque nous avons autorisé du SQL sans garantie dans la chaîne, la clause OR ajoutée par l’attaquant permet à toutes les lignes d’être renvoyées.
Cependant, c’est la moins effrayante des attaques. Imaginez ce qui arriverait si l’attaquant avait soumis "'; DELETE FROM user_contacts WHERE 'a' = 'a'". Nous terminerons avec cette requête complète:
SELECT * FROM user_contacts WHERE username = ''; DELETE FROM user_contacts WHERE 'a' = 'a';
Argggg ! Où est passée notre liste ?
La solution
Bien que ce problème soit insidieu et parfois difficile à localiser, la solution est simple: ne jamais faire confiance aux données soumises par l’utilisateur, et toujours les traiter lorsqu’elles sont injectées dans du SQL.
L’API de base de données de Django fait cela pour vous. Elle échappe automatiquement tous les paramètres SQL spéciaux, selon les conventions du serveur de base de données que vous utilisez (par exemple, PostgreSQL ou MySQL).
Par exemple, dans cet appel à l’API:
foo.get_list(bar__exact="' OR 1=1")
Django échappera l’entrée, pour générer une instruction resultante ressemblant à ceci:
SELECT * FROM foos WHERE bar = '\' OR 1=1'
Complétement inoffensif.
Ceci s’applique à l’intégralité de l’API de base de données de Django, avec quelques exceptions:
- L’argument where de la méthode extra() (voir l’annexe C). Ce paramètre accepte du SQL brut nativement.
- les requêtes faites «à la main» en utilisant l’API de base de données bas niveau.
Dans chacun de ces cas, il est facile de rester protéger. Pour chaque cas, évitez l’interpolation de chaîne à la faveur du passage de paramètres liés. Ceci étant, l’exemple qui démarre cette section doit être écrit ainsi:
from django.db import connection
def user_contacts(request):
user = request.GET['username']
sql = "SELECT * FROM user_contacts WHERE username = %s;"
cursor = connection.cursor()
cursor.execute(sql, [user])
# ... do something with the results
La méthode de bas niveau execute accepte une chaîne SQL avec les substitutions %s puis échappe et insert automatiquement les paramètres depuis la liste passée en second argument. Vous devriez toujours constuire du SQL personnalisé ainsi. Malheureusement, vous ne pouvez utiliser des paramètres liés partout dans le SQL; ils ne sont pas autorisés en tant qu’identifiant (c’est à dire, table ou nom de colonne). Aussi, si vous devez, admettons, contruire dynamiquement une liste des tables depuis une variable POST, vous devrez échapper ce nom dans votre code. Django fournit une fonction, django.db.backend.quote_name, qui échappera l’identifiant selon le schéma d’intégration de la base de données courante.
Cross-Site Scripting (XSS)
Le Cross-site scripting (XSS) se trouve dans les applications web qui échouent, lors de l’échappement du contenu soumis par l’utilisateur, avant de faire le rendu en HTML. Ceci permet à un attaquant d’insérer du HTML arbitraire dans les pages web, généralement sous forme de balises <script>.
Les attaquants utilisent souvent des attaques XSS pour voler les cookies et les informations de session, ou tromper les utilisateurs afin qu’ils donnent des informations privées à la mauvaise personne (alias phishing).
Ce type d’attaque peut prendre différentes formes et connait des permutations infinies, aussi regarderons nous juste un exemple typique. Considerez cette vue «Helle World» extrement simple:
def say_hello(request):
name = request.GET.get('name', 'world')
return render_to_response("hello.html", {"name" : name})
Cette vue lit simplement un nom depuis un paramètre GET puis le transmet au gabarit hello.html. Nous devons écrire un gabarit pour cette vue, comme suit:
<h1>Hello, {{ name }}!</h1>
Ainsi si nous accédons à l’adresse http://example.com/hello/name=Jacob, la page renvoyée contiendra ceci:
<h1>Hello, Jacob!</h1>
Mais, attendez - que se passe-t-il si nous appelons http://example.com/hello/name=<i>Jacob</i>? Alors nous obtenons ceci:
<h1>Hello, <i>Jacob</i>!</h1>
Évidemment, un attaquant n’utilisera pas quelque chose d’aussi bénin que les balises <i>; Il pourrait inclure un jeu entier de HTML qui détournerait votre page avec du contenu arbitraire. Ce type d’attaque a été utilisé pour tromper des utilisateurs afin qu’ils saisissent des données dans ce qui ressemblait à leur site banquaire, mais qui en fait était un formulaire détourné par XSS permettant de soumettre leurs informations de compte à un attaquant.
Le problème s’aggrave si vous stockez ces données dans la base de données pour ensuite l’afficher sur votre propre site. Par exemple MySpace s’est retrouvé vulnérable à une attaque XSS ce cette nature. Un utilisateur a inséré du Javascript dans son profil pour l’ ajouter automatiquement en tant qu’ami lorsque vous visitiez sa page de profil. En quelques jours, il a obtenu des millions d’amis.
Ceci peut sembler relativement bénin, mais gardez à l’esprit que cet attaquant manoeuvrait pour que son code - et pas celui de MySpace - s’exécute sur votre ordinateur. Ceci viole la confiance assumée que tout le code de MySpace est en réellement écrit par MySpace.
MySpace à été extrémement chanceux puisque ce code malicieux n’effaçait pas automatiquement les compte de visiteurs, ou modifiait leur mot de passe, ou inondait le site avec du spam, ou tout autre scénario cauchemardesque révélé par cette faille.
La solution
La solution est simple: toujours échapper n’importe quel contenu qui pourrait provenir d’un utilisateur. Si nous réécrivons simplement notre template:
<h1>Hello, {{ name|escape }}!</h1>
alors nous ne sommes plus vulnérables. Vous devez toujours utiliser la balise escape (ou quelque chose d’équivalent) lorsque vous affichez du contenu soumis par l’utilisateur sur votre site.
Pourquoi Django ne fait-il pas cela pour vous ?
Modifier Django pour qu’il échappe automatiquement toutes les variables affichées dnas les gabarits est un sujet de discussion fréquent sur la liste de diffusion des développeurs Django.
Jusqu’à présent, les gabarits Django ont évités cette fonctionnalité parce qu’elle modifie subtilement ce qui devrait être une fonctionnalité relativement directe (afficher les variables). C’est un problème épineux et un engagement difficilement évaluable. Ajouter des fonctionnalités implicites cachées est contre l’idéal des fondations de Django (et de Python, d’ailleurs), même si la sécurité est également importante.
Tout ceci pour dire, finalement, qu’il y a une chance que Django développe une forme d’auto-échappement (ou assimilé) dans ses fonctionnnalités à l’avenir. C’est une bonne idée de consulter la documentation officielle pour connaitre les dernières fonctionnalités de Django; elle sera toujours plus à jour que ce livre, et tout spécialement pour l’édition imprimée.
Même si Django ajoute cette fonctionnalité, vous devrez pouvoir continuer à avoir l’habitude de vous demander, à chaque fois, «d’où proviennent ces données ?». Aucune solution automatique ne protègera à 100% votre site des attaques XSS.
Cross-Site Request Forgery
Les Cross-site request forgery (CSRF) arrivent lorsqu’un site web malicieux trompe les utilisateurs en chargeant à leur insu un URL depuis un site où ils sont déjà identifiés - tirant ainsi parti de leur status d’identifié.
Django propose nativement un outil vous protégeant de ce genre d’attaque. L’attaque en elle-même ainsi que ces outils sont tous deux abordés avec force détails au chapitre 14.
Détournement/Falsification de session
Ce n’est pas une attauqe spécifique, mais plutôt une classe générale d’attaques sur les données de sessions d’un utilisateur. Elle peut prendre de nombreuses formes:
une attaque man-in-the-middle, où l’attaquant espionne les données de session lors du transit sur le réseau filaire (ou sans fils).
un détournement de session, où l’attaquant utilise un ID de session (probablement obtenu grâce à une attaque man-in-the-middle) afin de prétendre être un autre utilisateur.
Un exemple de ces deux premières pourrait être un attaquant dans un cyber café utilisant le reseau sans fils de la boutique pour capturer un cookie de session. Il pourrait alors ensuite utiliser ce cookie pour se faire passer pour l’utilisateur original.
une attaque par substitution de cookie, où un attaquant écrase les données supposées être en lecture seule et stockées dans un cookie. Le chapitre 12 explique en détail comment les cookies fonctionnent, l’un des points important étant qu’il est trivial pour les navigateurs et les utilisateurs malicieux de modifier les cookies sans que vous le sachiez.
Il existe une longue histoire des sites web qui ont stockés un cookie du genre IsLoggedIn=1 ou même LoggedInAsUser=jacob. Il est extremement simple d’exploiter ce genre de cookies.
À un niveau plus subtile, ce n’est jamais une bonne idée de faire confiance à tout ce qui est stocké dans les cookies; vous ne savez jamais qui a pu les insérer ici.
la fixation de session, où un attaquant trompe un utilisateur en paramétrant ou en réinitialisant l’ID de session de l’utilisateur.
Par exemple, PHP autorise les identifiants de session dans l’URL (par exemple, http://example.com/?PHPSESSID=fa90197ca25f6ab40bb1374c510d7a32). Un attaquant qui Atrompe un utilisateur grâce à un clic sur un lien ayant un ID de session codé en dur obligera l’utilisateur à condserver cette session.
La fixation de session a été utilisée dans les attaques «phishing» pour tromper les utilisateurs en les invitant à saisir des informations personnelles pour un compte que possède l’attaquant. Il peut ensuite se connecter sur ce compte et récupérer les données.
L’intoxication de session, oùun attaquant injecte des données potentiellement dangeureuses dans la session d’un utilisateur - habituellement grâce à un formulaire web que l’utilisateur soumet pour déterminer des données de session.
Un exemple canonique est un site qui stocke une simple préférence utilisateur (comme une couleur d’arrière plan) dans un cookie. Un attaquant peut tromper un utilisateur grâce à un clic sur un lien permettant de soumettre une «couleur» qui contient en fait une attaque XSS; si cette couleur n’est pas échappée, l’utilisateur peut encore une fois injecter du code malicieux dans l’environnement de l’utilisateur.
La solution
Il y a de nombreux prinicipes généraux qui peuvent vous protéger de ces attaques:
ne jamais autoriser les informations de sessions contenues dans l’URL.
Le framework de session Django (voir le chapitre 12) ne permet tout simplement pas que les sessions soient contenues dans l’URL.
ne pas stocker les données directement dans les cookies; au lieu de cela, stockez un ID de session qui correspond aux données de sessions stockées en coulisses.
Si vous utilisez le framework de session natif sous Django (c’est à dire, request.session), ceci est automatiquement géré pour vous. Le seul cookie de session qu’utilise le framework est un ID de session unique; toutes les données de session sont stockées dans la base de données.
rappelez-vous d’échapper les données de session si vous les affichez dans le gabarit. Consultez la section précédente sur les XSS, et souvenez-vous que ceci s’applique à n’importe quel contenu créé par un utilisateur ainsi qu’à toutes les données provenant du navigateur. Vous devez considérer les informations de session comme étant crées par les utilisateurs.
prévenir les falsification d’ID de session par les attaquants à chaque fois que cela est possible.
Bien qu’il soit presqu’impossible de détecter quelqu’un ayant détourné un ID de session, Django possède nativement une protection contre les attaques de session par force brute. Les IDs de session sont stockés sous la forme de hachage (au lieu de nombres séquentiels), ce qui évite les attaques brute de force, et un utilisateur obtiendra toujours un nouvel ID de session s’il en essaie un qui n’existe pas, ce qui prévient la fixation de session.
Notez qu’aucun de ces principes et qu’aucun de ces outils prévient des attaques «man-in-the-middle». Ces types d’attaque sont presque impossible à détecter. Si votre site autorise les utilisateurs connectés à voir toute sorte de données sensibles, vous devriez toujours servir ce site sous HTTPS. EN complément à cela , si vous avez un site activant les SSL, vous devrez fixer le paramètre SESSION_COOKIE_SECURE à True; ceci pour que Django n’envoie de cookie de session qu’au travers de HTPPS.
Injection d’en-tête de courrier
L’injection SQL est bien moins connue que sa soeur, l’injection d’en-tête de courrier électronique, qui détourne les formulaires web envoyant des courriels. Un attaquant peut utiliser cette technique pour envoyer du spam via votre serveur de courrier. Tout formulaire qui construit des en-tête de courrier à partir des données provenant d’un formulaire web est vulnérable à ce genre d’attaque.
Jettons un oeil au formulaire de contact canonique que l’on trouve sur beaucoup de site web. Habituellement il envoie un message vers une adresse de courriel codée en dur et, par conséquant, ne semble pas à première vue vulnérable aux abus de spam .
Cependant, la plupart de ces formulaires autorisent aussi l’utilisateur à saisir son propre sujet pour le courrier (parmi l’adresse «De», le corps du message, et parfois quelques autres champs). Ce champs «sujet» est utilisé pour construire l’en-tête «subject» du message.
Si cet en-tête n’est pas échappé lors de la construction du message, un attaquant peut soumettre quelque chose du genre "hello\ncc:spamvictim@example.com" (où "\n" correspond au caractère nouvelle ligne). Ceci transformerait l’en-tête du courrier préparé en:
To: hardcoded@example.com Subject: hello cc: spamvictim@example.com
Comme pour l’injection SQL, si nous faisons confiance à la ligne sujet donnée par l’utilisateur, nous l’autoriserons à construire un jeu d’en-têtes malicieuses, il pourra alors utiliser notre formulaire de contact pour envoyer du spam.
La solution
Nous pouvons prévenir cette attaque de la même manière que lors de la prévention de l’injection SQL: toujours échapper ou valider le contenu soumis par l’utilisateur.
Les fonctions natives de courrier sous Django (dans django.core.mail) n’autorisent tout simplement pas les nouvelles lignes dans aucun des champs utilisés pour contruire les en-têtes (les adresses «De» et «À», plus le sujet). Si vous tentez d’utiliser django.core.mail.send_mail avec un sujet qui contient des nouvelles lignes, Django lévera une exception BadHeaderError.
Si vous n’utilisez pas les fonctions de courrier natives sous Django, vous devrez vous assurer que les nouvelles lignes dans les en-têtes engendrent soit une erreur, soit sont tout simplement supprimés. Vous pourriez vouloir examiner la classe SafeMIMEText dans django.core.mail pour voir comment Django fait cela.
Parcours de répertoire
Le Directory traversal est un autre style d’attaque par injection, où un utilisateur malicieux trompe le code du système de fichier en lisant ou écrivant des fichiers pour lesquels le serveur web ne devrait pas avoir accès.
Un exemple pourrait être une vue qui lit des fichiers depuis le disque sans assainir le nom du fichier:
def dump_file(request):
filename = request.GET["filename"]
filename = os.path.join(BASE_PATH, filename)
content = open(filename).read()
# ...
Bien qu’il semble que la vue restreigne l’accès aux seuls fichiers appartenant à BASE_PATH (en utilisant os.path.join), si l’attaquant transmet un filename contenant .. (il s’agit de deux points, un raccourci pour «le répertoire parent»), il peut accéder aux fichiers «sous» BASE_PATH. C’est juste une question de temps avant qu’il ne puisse découvrir le nombre correcte de points pour accéder avec succes, à ../../../../../etc/passwd par exemple.
Tout ce qui lit les fichiers sans échappement propre est vulnérable à ce problème. Les vues qui écrivent des fichiers sont tout aussi vulnérables, mais les conséquences sont doublement terribles.
Une autre permutation de ce problème réside dans du code qui charge dynamiquement les modules basés sur l’URL ou d’autres informations de requête. Un exemple ayant deffrayé la chronique nous est parvenu du monde Ruby on Rails. Un peu avant la mi-2006, Rails utilisait des URLs comme celle-ci http://example.com/person/poke/1 pour charger directement les modules et appeler les méthodes. Le résultat fut qu’une URL soigneusement construite pouvait automatiquement charger du code arbitraire, y compris un script de réinitialisation de la base de données !
La solution
Si votre code à toutefois besoin d’écrire ou de lire des fichiers selon l’entrée de l’utilisateur, vous devez purger le chemin requis très soigneusement pour vous assurer qu’un attaquant ne soit pas autorisé à sortir du répertoire de base sur lequel vous restreignez l’accès.
Note
Il n’est pas besoin de dire que vous ne devez jamais écrire du code qui puisse lire n’importe quel partie du disque !
Un bon exemple pour savoir comment faire cet échappement réside dans la vue native sous Django et servant du contenu (dans statique django.views.static). Voici le code significatif:
import os
import posixpath
# ...
path = posixpath.normpath(urllib.unquote(path))
newpath = ''
for part in path.split('/'):
if not part:
# strip empty path components
continue
drive, part = os.path.splitdrive(part)
head, part = os.path.split(part)
if part in (os.curdir, os.pardir):
# strip '.' and '..' in path
continue
newpath = os.path.join(newpath, part).replace('\\', '/')
Django ne lit pas les fichiers (à moin que vous n’utilisiez la fonction static.serve, mais elle est protégée avec le code que nous venons de voir), ainsi cette faille n’affecte pas tellement le code structurel.
En complément, l’usage de l’abstraction URLconf signifie que Django ne chargera jamais du code que vous ne lui aillez explicitement indiquer de charger. Il n’y a aucun moyen de créer une URL qui oblige Django à charger quelquechose qui ne soit pas mentionné dans un URLconf.
Messages d’erreur exposés
Pendant le développement, être capable de voir les traces et les erreurs directement dnas votre navigateur est extrêmement util. Django possède de «jolis» et d’instructifs messages d’erreur tout spécialement pour rendre le débuguage plus facile.
Cependant, si ces erreurs sont affichées une fois le site en production, elle peuvent révéler des aspects de votre code ou de votre configuration qui peut aider un attaquant.
Par ailleurs, les erreurs et les traces sont totalement inutiles aux utilisateurs finals. La philosophie de Django est que les visteurs du site ne doivent jamais voir les meesages d’erreur relatifs à l’application. Si votre code lève une exception non gérée, un visiteur ne doit pas voir la trace complète - ou tout autre échantillon de code ou de message d’erreur Python (à destination des programmeurs). Au lieu de cela, le visiteur doit voir un message «Cette page n’est pas disponible» amical.
Naturellement, les développeurs ont besoin de voir les traces pour résoudre les problèmes dans leur code. Aussi le framework doit masquer tous les messages d’erreur au public, mais il doit aussi les afficher aux développeurs de confiance du site.
La solution
Django a un simple drapeau qui contrôle l’affichage de ces messages d’erreur. Si le paramètre DEBUG est à True, les messages d’erreur seront affichés dans le navigateur. Sinon, Django renverra un message HTTP 500 («Erreur interne au serveur») et rend le gabarit d’erreur que vous fournissez. Ce gabarit d’erreur est appellé 500.html et doit se situer à la racine de l’un de vos repertoires de gabarit.
Parce que les développeurs doivent toujours voir les erreurs générées sur un site en production, toutes les erreurs gérées de cette façon enverrons un courrier avec la trace complète à toutes les adresses données dans le paramètre ADMINS.
Les utilisateurs déployant sous Apache et mod_python doivent aussi s’assurer qu’ils ont PythonDebug Off dans leurs fichiers de configuration d’Apache; cela supprimera toutes erreurs se produisant avant que Django ait eu une chance de les charger.
Un dernier mot sur la sécurité
Nous espérons que toute cette discution sur les problèmes de sécurité n’est pas trop intimidante. Il est vrai que le web peut être un monde trouble et sauvage, mais avec un peu de prévoyance, vous pouvez obtenir un site web sécurisé.
Gardez à l’esprit que la sécurité sur le web est un champs en constant changement; si vous lisez la version figée de ce livre, assurez vous de consulter des ressources plus à jour en matière de sécurité en particulier au sujet de toute nouvelle faille qui aurait pu être découverte. En fait, c’est toujours une bonne idée de passer quelque temps une fois par semaine ou une fois par mois à rechercher et à rester à jour sur l’état de l’art en matière de sécurité des applications web. C’est un petit investissement à faire, mais la protection que vous obtiendrez pour votre site et vos utilisateurs est sans prix.
Et ensuite ?
Dans le chapitre suivant nous aborderons finalement les subtilités du déploiement de Django: comment lancer un site en production et comment le paramétrer en prévoyant des évolutions.
Dernière modification: 2008-08-04 13:36:41.349226