Portrait de Philippe PradosPrados.FRProgrammation, architecture & sécurité — depuis 1998

Développement sécurisé

Cet article évoque les différents pièges à éviter lors du développement d’un site Internet, afin de résister aux attaques les plus classiques, tentés par les pirates.

Par Philippe PRADOS - 2003
www.prados.fr

Les développeurs de site Internet ne sont pas formés aux risques de sécurité. Il présente alors de nombreuses vulnérabilités. Le développeur doit avoir à l’esprit différentes familles de vulnérabilités afin de protéger son application. Plusieurs risques sont inhérents aux protocoles utilisés par un site WEB ou aux technologies couramment utilisées.

Lors du développement, il faut considérer

L’authentification

La première phase importante est l’authentification de l’utilisateur. Plusieurs précautions sont à prendre lors de cette phase. On peut les résumer ainsi :

D’autres critères permettent de se protéger des attaques en forces brutes ou de contraindre une utilisation limitée de l’identité.

Traiter la navigation

La phase suivante à traiter consiste à contrôler rigoureusement la navigation de l’utilisateur. C’est une phase difficile, car le serveur n’a aucun contrôle sur le navigateur de l’internaute. Il peut ouvrir simultanément plusieurs fenêtres, soumettre plusieurs requêtes en même temps, naviguer en suivant un chemin très éloigné que celui prévu par l’application.

Une première approche consiste à n’avoir qu’une seule URL pour l’application. Le serveur se charge de livrer la page correspondante, au fur et à mesure de la navigation. Cette approche interdit d’exploiter les techniques de cache du protocole http ou plusieurs fenêtres pour la même application. Il faut également que l’application interdise la soumission simultanée de plusieurs requêtes par le même utilisateur.

Une autre approche consiste à ajouter une valeur aléatoire dans chaque page. Celle-ci doit être présentée lors de la demande des nouvelles pages. Ainsi, une page n’est livrée que si elle vient d’une page précédemment généré par l’application. Cela complique fortement l’application, car chaque lien, chaque formulaire, doit intégrer cette valeur aléatoire. Il n’est plus possible de demander un « back » dans le navigateur.

Qualifier les paramètres

Les paramètres des formulaires arrivent en forme brute dans l’application. Il peut y avoir de nombreux caractères étranges, plus ou moins attendus par l’application. Après une phase de nettoyage, les données brutes sont disponibles dans des variables. Ces données ne sont jamais dignes de confiance. Il n’est pas question de les exploiter sans y apporter des traitements spécifiques à chaque usage.

Une donnée brute sert généralement à plusieurs choses. Par exemple, une requête SQL, un flux XML, un flux XSL, un fichier plat, une requête LDAP ou un fragment HTML peut être généré. Dans chaque situation, il faut appliquer les règles spécifiques d’encodages. J’ai personnellement identifié X encodages différents dans les applications auxquelles je participe.

Il existe bien d’autres encodages suivant les technologies utilisées.

Les développeurs utilisent rarement qu’un seul langage. Il y a très souvent une génération d’un bout de langage à partir d’information brute. C’est le cas lorsque le programme génère une page HTML, une requête SQL, un flux XML ou une requête LDAP. Cette génération est souvent oubliée par les développeurs. Ils n’imaginent pas que toute la puissance du langage produit peut être exploitée par un pirate. L’approche naïve consistant à supprimer tous les caractères illégaux dès l’entrée de l’information ne fonctionne pas. En effet, le programme doit être capable de traiter des utilisateurs s’appelant « O’Reilley ». Certains caractères sont trop suspects pour être honnêtes. C’est le cas du caractère de la valeur zéro. Il sert généralement de marqueur de fin de chaîne de caractère. Cela peut être exploité par un pirate pour contourner des règles de filtrages.

Flux de traitement

Les paramètres étant correctement utilisés, il faut maintenant vérifier que les traitements s'effectuent comme il faut, et dans l’ordre prévu. Les applications complexes demandent des informations sur plusieurs pages, les manipulent, les agrègent, avant de demander le traitement unitaire ayant un impact sur le système d’information. Il ne faut jamais considérer que les vérifications effectuées précédemment empêchent un pirate d’agir. Il existe tellement de technique pour contourner les tests, qu’il est indispensable de reprendre toutes les vérifications depuis le début, et dans une transaction, avant de modifier le système d’information.

Gestion des erreurs

De nombreuses erreurs peuvent arriver dans une application. Certaines sont de véritables bogues, c'est-à-dire des situations non prévues par le développeur, d’autres sont des erreurs technologiques comme la perte de la communication avec la base de données, la saturation du disque dur, etc. Dans tous les cas, il ne faut pas transmettre d’information à l’utilisateur lui permettant d’identifier les composants sous-jacents de l’application. Retourner la requête SQL ayant échoué est une bonne idée pour le développeur, mais une très mauvaise si l’on souhaite se protéger des pirates.

Prévention

Des traitements supplémentaires permettent d’identifier une attaque de pirate.

Vous pouvez ajouter un script de gestion des erreurs 404 pour détecter les demandes de scripts réputés pour leurs failles. Lorsqu’un pirate demande une URL qui n’existe pas, vérifiez que celle-ci ne correspond pas à l’invocation d’un script connu. Si c’est le cas, il est probable qu’un pirate est en train d’analyser votre site. Vous pouvez envoyer un message automatiquement à l’administrateur pour qu’il réagisse rapidement. Des sites référencent des listes de scripts vulnérables.

ScriptAlias /abuse_trap /usr/lib/cgi-bin/abuse_trap.pl
<Directory /user/local/cgi-bin>
ErrorDocument 404 /abuse_trap
</Directory>

Le programme peut être enrichi des vérifications suivantes :

Des produits commerciaux permettent d’automatiser cela.

Déni de service

L’application peut être un chemin pour obtenir un déni de service et rendre ainsi le serveur inutilisable. Indépendamment des attaques réseaux, Plusieurs techniques sont possibles :

Les algorithmes sont prévus pour être efficaces dans le cas moyen, mais pas pour le pire. Un pirate peut volontairement utiliser des informations plaçant celui-ci dans les pires conditions. Avec peu de volume, il peut alors avoir un impact très important sur l’application.

Par exemple :

Par exemple, pour analyser des expressions régulières, plusieurs familles de moteurs peuvent être utilisés : Les NFA ou les DFA. Il s’agit d’une différence d’approche dans l’analyse de l’expression. Un moteur NFA analyse tous les chemins possibles de l’expression, et retourne en arrière lors d’un échec. Un moteur DFA maintient une liste des candidats encore possible, et l’élague au fur et à mesure des caractères rencontrés. Chaque caractère est analysé qu’une seule fois. Un moteur DFA est plus complexe à compiler, mais plus rapide à l’exécution. Un moteur DFA est plus rapide à compiler mais moins rapide à l’exécution. Il est fortement dépendant de la rédaction de l’expression régulière, alors qu’un moteur DFA en est indifférent.

Par exemple, une expression (int|info) peut être optimisée en in(t|fo) pour un moteur NFA. Ainsi, en cas de retour arrière, les deux premières lettres peuvent être considérées comme possibles. Il n’est pas nécessaire d’analyser une nouvelle fois l’ensemble des caractères.

L’application utilise un moteur NFA, sensible à la syntaxe de l’expression régulière. Suivant les cas, le temps d’analyse d’un paramètre peut être linéaire par rapport à la taille de la chaîne O(n), quadratique O(n²), cubique O(n3) ou exponentielle O(nn).

ExempleType
a*Linéaire
a*[ab]*OQuadratique
a*[ab]*[ac]*OCubique
(a|aa)*OExponentielle

Exemple

Type

a*

Linéaire

a*[ab]*O

Quadratique

a*[ab]*[ac]*O

Cubique

(a|aa)*O

Exponentielle

Un déni de service peut être obtenu en envoyant, en grande quantité, des valeurs suffisamment longues pour entraîner un travail excessif du serveur. Cela occasionne de nombreux retours arrière. Par exemple, dans le cas d’une expression exponentielle, une chaîne de trente caractères peut être analysée en une minute en consommant 100% de la CPU.

Pour vous en convaincre, consultez la démonstration ici : http://jakarta.apache.org/oro/demo.html.

Indiquez l’expression AWK (moteur DFA) (a|aa)*O, demandez matches(), indiquez la valeur aaaaaaaaaaaaaaaaaaaZ et lancez le traitement. Vous obtenez rapidement un résultat. Modifiez alors l’algorithme utilisé en Perl5 (moteur NFA), et lancez à nouveau le traitement. Suivant la taille de la chaîne, le résultat peut mettre un temps très important pour analyser l’expression. Notez que le moteur DFA proposé par ORO ne gère pas les caractères unicodes.

Il faut limiter la taille des données avant d’appliquer une expression régulière. Optimisez également les expressions régulières pour éviter au maximum les retours arrière lors de l’analyse d’une chaîne.

Les requêtes non déterministes doivent avoir un temps maximum d’exécutions. Il faut limiter le temps limite d'exécution d’une requête à la base de donnée à une valeur raisonnable, compatible avec les autres timeouts du serveur d’application.