De la sécurité des applications web

Je suis retombé il y a peu de temps sur quelques messages échangés avec le développeur d'un jeu en ligne sur plateforme LAMP (Linux Apache MySQL PHP). C'était entre février et juin 2004, nous (#sden "canal historique") avions beaucoup trop de temps libre, et pas assez de choses intelligentes à faire. À l'époque, le concepteur du jeu ajoutait de nouvelles options et fonctions presque quotidiennement, signe que le code derrière ne devait pas être bien propre. Comme le jeu imposait une limite d'actions par unité de temps, et que tout ceci n'allait pas assez vite pour nous qui passions des journées entières rivés à l'écran, il a fallu prendre les choses en main.
On lui a tout fait, le pauvre. Tout, de l'empoisonnement de variables aux injections SQL. Avec à chaque fois un rapport de bug, bien sûr, nous ne voulions pas avoir le mauvais rôle.
Le plus dur a sans doute été de faire accepter à ce développeur d'applications compilées locales, qu'une application interprétée en ligne ne peut pas se programmer avec laxisme.
Le pire dans tout ça, c'est que le genre d'erreurs grossières de programmation commises par ce développeur est encore monnaie courante. La dernière faille de wordpress en est un parfait exemple.
Alors bien sûr, il existe maintenant des outils sophistiqués pour lutter contre certaines attaques. mod_security pour Apache permet par exemple de bloquer toute forme d'injection SQL, certains empoisonnements de variable et d'autres types d'attaques. Pour être efficace il réclame néanmoins un paramétrage complet calqué sur le fonctionnement de votre application.
Il serait tellement plus efficace de bien coder son application dès le départ !
Sans prétendre être exhaustif, voici quelques recommandations :

Configurez votre serveur web en mode "production", de sorte qu'il n'affiche pas sa version ni la version des modules chargés. Sur Apache, cela se passe dans le httpd.conf sous la forme ServerTokens Prod.
Ainsi, votre serveur est identifié sous le nom Apache au lieu de Apache/2.2.11 (Unix) mod_ssl/2.2.11 OpenSSL/0.9.7l DAV/2 PHP/5.2.8 SVN/1.4.4

Si c'est possible, configurez votre firewall pour qu'il ne laisse pas sortir l'utilisateur "apache" (en général www, ou httpd), sur d'autres ports que les ports 80 et 443. Sous PF (freebsd/openbsd) cela peut se faire comme ceci :

pass out quick proto tcp from any port 80 to any user 80 keep state
pass out quick proto tcp from any port 443 to any user 80 keep state
block out log proto { tcp, udp } user 80

Si vous ne pouvez pas faire ce blocage dans le firewall, faites au moins en sorte que votre interpréteur ne puisse pas ouvrir de sockets, ni accéder à n'importe quel fichier du serveur.

Configurez votre interpréteur (souvent PHP) pour ne pas retourner les erreurs. Au pire, créez un virtual host de développement pour vous-seul, qui affiche les erreurs, et assurez vous que les erreurs ne s'affichent pour personne d'autre. Utilisez log_errors = On dans votre php.ini pour enregistrer les erreurs dans un fichier désigné par error_log = /var/log/php_error.log.

Ne faites jamais confiance à l'utilisateur. Il arrivera toujours un moment où il va tripoter l'URL pour changer un paramètre, il peut aller jusqu'à modifier un formulaire pour injecter du code SQL dans une variable postée.
Ne faites pas confiance à javascript pour contrôler la validité des données. Contrôlez toujours côté serveur que les nombres sont des nombres, les chaînes de caractères sont bien ce qu'elles doivent être…
Si vous devez transmettre des données importantes via cookie/url/autre, n'hésitez pas à les chiffrer, ou à les accompagner d'un hash pour détecter une altération éventuelle.
Le referer (l'URL de provenance) est une donnée fournie par l'utilisateur. Ne bâtissez aucun système de sécurité à partir du referer.

Ne vous méprenez pas, il est possible d'attaquer un serveur (par injection SQL notamment) en aveugle. Même si aucune erreur n'est retournée par PHP, la page sans injection sera différente de la page avec injection, et cela suffit en général pour savoir si le code injecté est bien interprété. Il est donc vraiment vital de contrôler les données envoyées par l'utilisateur.
Par essais successif, il peut être possible de lire des informations dans la base de données. Par exemple, on peut créer une injection SQL qui va juste comparer le Nième caractère d'un champ de la table avec un caractère qu'on propose. Il suffit de faire en sorte que la requête SQL échoue si le caractère est juste et ne fasse rien si le caractère n'est pas le bon (ou inversement). Suivant la page html obtenue, même sans affichage d'erreur, on saura quel caractère est en quelle position dans le champs de la table.
Imaginons que mon champ contienne un mot de passe en clair de 10 caractères (c'est MAL). Cela fait un maximum de 10x255 caractères à tester, si on s'en tient à l'ASCII. Via injection SQL, j'aurai donc au maximum 2550 tests à faire pour trouver le mot de passe de 10 caractères. Si je décide non pas de tester le caractère, mais plutôt les bits qui constituent ce caractère, je n'ai plus que 8 tests à faire. En 80 tentatives au maximum, je trouve le mot de passe. Rapide, efficace, et surtout sensiblement plus discret.

À lire aussi :

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *