10 astuces pour éviter des failles de sécurité

Par @jbnahan69
Security Solutions - Global Security - Online Security - House Security https://www.pxfuel.com/en/free-photo-jrpjv

Pour plus de simplicité, j'utilise le terme application pour désigner les applications web et les sites web (par exemple, un site vitrine, etc.).

Filtrer, filtrer, filtrer

La première astuce consiste à filtrer côté serveur Web toutes les données reçues de l'utilisateur.

Si vous développez une librairie, les entrées correspondent à tous les arguments des méthodes publiques mise à disposition du développeur qui utilise votre librairie.
Il est également nécessaire de bloquer les contournements possibles de vos protections en prévoyant des points d'extension contrôler et en bloquant la modification du fonctionnement de votre code.

Si vous développez une application, les entrées utilisateurs sont toutes les données reçues via une requête HTTP, les arguments reçus lors de l'exécution d'une commande dans le terminal.

Pour filtrer, il existe différentes méthodes selon les cas.

  • Un texte libre où l'utilisateur est libre de saisir ce qu'il souhaite, un filtre avec une expression rationnelle est efficace, mais ne suffit pas. J'en reparlerai plus bas.
  • Une date
  • Un numéro de téléphone
  • Une couleur
  • Un entier ou nombre décimal
  • Une case à cocher
  • Une liste de choix

Contrôler les URL fournies

Cette astuce est d'un niveau supérieur, il existe une donnée qui est extrêmement difficile de contrôler, c'est les URL.

Si votre application demande à l'utilisateur une URL pour faire un rappel (callback) ou charger des données qui seront affichées plus tard (ex.: agrégateur RSS) vous avez un risque d'exposer vos API internes.

Il existe des services sur Internet permettant de tromper les vérifications DNS. Par exemple, le domaine 192.168.0.1.fake-dns.com passeront les filtres de validité de l'URL, mais lors de la résolution DNS enverra la requête sur le réseau interne.

Certains services proposent de résoudre le nom de domaine avec deux IP fourni l'une après l'autre... Ainsi lors de la première résolution, l'IP retourné est bien une IP externe, mais lors de la seconde résolution (cette fois pour envoyer la requête) l'IP retourné par le DNS est une IP interne.

Extrême prudence donc sur les URL fournies pas les utilisateurs.

Il existe selon les clients HTTP la possibilité de fournir la liste des plages IP interdite permettant de limiter les accès. Cependant, comme indiqué plus haut l'efficacité est liée à l'implémentation.

À mon sens réaliser une résolution DNS manuelle (pour filtrer) avant l'exécution de la requête est une bonne chose uniquement s'il est possible de fournir au client HTTP l'IP du serveur sur lequel se connecter indépendamment du hostname.

La validation navigateur n'est pas suffisante

Avec l'évolution du développement coté navigateur, nombreux sont ceux qui font confiance au filtre mis en place avec JavaScript. Ces filtres sont une bonne chose, mais s'ils ne sont pas également exécutés sur le serveur qui reçoit les données cela ne vous protège de rien.

Voici un scoop, les malveillants n'utilisent pas de navigateur pour exfiltrer les données. Les filtres JavaScript ne sont donc pas exécutés et votre serveur est vulnérable !

Préparer vos requêtes SQL

Cette astuce consiste à protéger les éléments variables que vous utilisez dans vos requêtes SQL.

Pour toute requête, les données utilisées pour la valeur des colonnes des tables doivent être passées en argument avec une requête préparer. Je vous laisse lire la documentation de la librairie que vous utilisez pour accéder aux données.

Cette pratique est assez courante, mais il reste des éléments qu'il n'est pas possible de passer en argument d'une requête préparée.

Par exemple, le nom de la colonne à utiliser pour le tri, le sens du tri, les noms des colonnes à retourner, le nom de la table à interroger. Pour ces éléments, vous devez les filtrer (et oui encore) avec une liste blanche.

Par exemple, la valeur du sens de tri est égale à ASC ? Alors utilisation de la valeur ASC, sinon utilisation de la valeur DESC. Le nom de la colonne fourni pour le tri est-il dans la liste des colonnes autorisées pour le tri ? Si oui, alors la colonne est utilisée, sinon, la valeur par défaut est utilisée.

Échapper l'affichage

Cette astuce est extrêmement simple à mettre en place. Toute donnée affichée dans les templates (pour une réponse HTTP) doit être échappée.

C'est-à-dire que si le texte à afficher contient le caractère < il sera remplacé part &lt;. L'affichage est correct et s'il contient une balise HTML utilisée pour réaliser une injection XSS le code sera affiché au lieu d'être exécuté par le navigateur.

L'échappement évitera également l'hébergement de site peu recommandable sur votre site grâce à l'injection de code HTML dans votre page.

Gérer correctement les fichiers téléversés

La première astuce est de limiter les types de fichiers autorisés et la taille. La vérification de ces informations doit être réalisée sur le serveur sans tenir compte des informations fournies par l'utilisateur (ou son navigateur).

La seconde est de ne pas stocker le fichier avec le nom original sur votre serveur. Calculer une somme de contrôle unique ou générer des données aléatoires pour être sûr du nom du fichier.

La gestion des fichiers uploadés par les utilisateurs de votre application est différente en fonction de ces deux cas :

  • les fichiers publics
  • les fichiers privés

Pour les fichiers publics, le stockage du fichier sera réalisé en général dans un dossier public. Ainsi le fichier sera plus simple à télécharger. L'exécution des fichiers présents dans le dossier de dépôt doit être désactivée !

Pour les fichiers privés, le stockage du fichier sera réalisé dans un dossier privé (inaccessible de l'extérieur via le serveur web). Le téléchargement passera forcément par votre application pour vérifier les accès (j'y reviendrai plus bas). Il est en général nécessaire de stocker certaines informations en base de données (tel que le nom original du fichier, le type, la taille, le nom réel utilisé sur l'espace de stockage).

Si vos fichiers privés sont hébergés sur un service de stockage distant (ObjectStorage, S3), bien vérifier les droits d'accès à l'espace de stockage.

Stocker correctement les mots de passe

Cette astuce consiste à rendre illisibles les mots de passe de vos utilisateurs stockés en base de données. Illisible, mais également difficile à retrouver. C'est pourquoi les méthodes de hachage tel que MD5, SHA1 et suivante sont à proscrire.

À la place, utilisez une méthode de hachage qui contient un paramètre de temps de calcul.

Imaginons que le calcul du hash d'un mot de passe prend sur votre serveur 5ms. Lors de la vérification du mot de passe, l'utilisateur ne le sentira pas. Par contre, si vous avez 50 000 mots de passe dont il faut calculer le mot de passe, le temps n'est plus le même.

Un malveillant sera capable de mettre beaucoup d'argent et de temps pour casser les mots de passe s'il est sûr que cela lui rapportera gros.

Pour terminer cette astuce, lors de la vérification du mot de passe, utilisez la méthode de comparaison fournie par la librairie. Il existe des failles basées sur le temps lors de la vérification du hash.

Protéger les actions dangereuses

Cette astuce consiste à sécuriser les actions réalisées par les utilisateurs de votre application au travers d'un formulaire.

Lorsque votre utilisateur ajoute, modifie ou supprime une donnée, vous devez vous assurer que l'action vient bien de lui. Pour cela, lors de la génération de la page du formulaire, ajouter un champ caché contenant un code unique aléatoire (token CSRF) que vous stockez également dans la session de l'utilisateur côté serveur.

Lors de la soumission du formulaire par l'utilisateur, la valeur du champ caché doit être égale à la valeur stockée en session.

Cette astuce est plutôt répandue (grâce aux frameworks qui l'intègre) pour les formulaires d'ajout et modification, mais pas pour les actions dangereuses qui ne passent pas par un formulaire telle la suppression.

Dans le cas de la suppression ou de toute autre action (activation/désactivation par exemple) il est possible de transformer le lien en formulaire et ainsi de positionner le token.

Mais il est également possible lors de la génération du lien de générer un token qui sera passé en paramètre GET de la requête.

Pourquoi tout cela ?

Il est très simple pour un malveillant de générer une page web contenant une image dont l'URL est celle d'une action dangereuse de votre application. Lors du chargement de la page malveillante, votre navigateur réalisera une requête vers votre application pour charger l'image. Si vous êtes connecté à l'application, l'action sera réalisée sans que vous le sachiez.

Voici à quoi peut ressembler une image piégée. Cela fonctionne également avec les courriels si votre client charge les images par défaut.

<img src="https://votre_application.fr/user/10/delete">

Le code ne fonctionnera pas si un token est nécessaire (ex.: https://votre_application.fr/user/10/delete?token=2528425faf41) car la valeur du token n'est pas devinable !

Contrôler les accès

Cette astuce consiste à vérifier si l'utilisateur actuel à le droit de voir la page demandée. Dans le cas ou l'utilisateur n'est pas connecter, une erreur HTTP 401 sera renvoyé et dans le cas ou l'utilisateur est connecté, mais qu'il n'a pas les accréditations une erreur HTTP 403 sera renvoyé.

Vérifier l'accès à une page est une chose, mais parfois un utilisateur à l'accréditation pour accéder à une page, mais pas pour toutes les données.

Prenons le cas de l'affichage des données d'un utilisateur. L'URL est sous la forme https://monapplication.fr/user/<id>.

L'utilisateur courant peu visualiser la page de profils uniquement des utilisateurs qui sont dans le même groupe que lui. Il a donc l'accréditation pour accéder à l'URL.

Lors du traitement de la requête, le serveur doit vérifier que l'utilisateur courant est dans le même groupe que l'utilisateur qu'il souhaite afficher. S'il n'est pas dans le même groupe, une erreur HTTP 403 sera renvoyée.

Cette vérification est également à inclure dans les tests automatisés de votre application.

Protéger vos cookies

Cette astuce consiste à bloquer l'utilisation du cookie de session ou reconnexion automatique (remember me) par JavaScript et lors d'un clic sur un lien (email ou un autre site).

Lors de l'envoi d'un cookie au navigateur, il y a des drapeaux (flags) qui peuvent être activés. Activer le drapeau HttpOnly pour que JavaScript n'y accède pas et ainsi éviter le vol des cookies de session.
Activer le drapeau Secure pour que le navigateur émettre le cookie que pour les requêtes HTTPS.
Activer le drapeau SameSite=Strict si votre application n'utilise pas de système d'authentification tiers (SSO). Cette option expérimentale bloquera des attaques CSRF (voir "Protéger les actions dangereuses".

N'utilisez pas les index auto-incrément

Cette astuce consiste à ne pas utiliser d'identifiant auto incrémental. Pourquoi ? Lors de l'accès aux données, il est fréquent de mettre l'identifiant dans l'URL.
Si l'accès à la donnée est protégé, il est facile de tenter une autre URL pour essayer d'accéder à des données auquel il n'est normalement pas possible d'accéder.

Voici l'exemple de l'URL d'accès aux informations d'un client de ma base client héberger sur un service SaaS https://monservice.fr/client/10.

Maintenant, je sais que l'ID de mon premier client est 10 et le dernier est 20. Qu'y a-t-il avant et après ? Il est très facile de le découvrir.

Si vous avez appliqué l'astuce "Contrôler les accès" le problème ne se posera pas.

Il ne restera qu'une possible estimation de la quantité de données disponibles dans la table.

Oui, il est encore possible de déterminer le dernier index utilisé, car une fois dépassé, l'erreur HTTP sera 404 (introuvable) à la place de 403 (accès refusé).

C'est ici que les identifiants non incrémentaux générés de façon sécurisée sont efficaces, car non prévisibles.

Protéger vos API

Toutes ces astuces sont applicables aux API HTTP avec une nuance pour la protection des actions par token temporaire (CSRF). Vos API doivent être protégés par un token ajouté dans les en-têtes des requêtes HTTP.

Ce token est soit récupéré après l'authentification de l'utilisateur. Sois généré une fois et restera valide tant qu'il est connu du serveur.

Grâce à ces tokens, il est possible de limiter les accès à vos API (nombre de requêtes possibles dans un laps de temps).

Conclusion

La sécurité des données et de l'information en général est l'affaire de tous. Nous pouvons tous faire quelque chose à notre niveau.

En tant que développeur, nous sommes en amont des futures fuites de données et nous devons faire notre possible pour les éviter, informer nos collègues et nos responsables sur la nécessiter de sécuriser les données. Avec de petites astuces, il est possible de réduire les risques !

Je n'ai pas parlé de langages de programmation dans ces astuces... Dites-moi dans les commentaires si vous êtes intéressé par une explication des failles pour PHP, mais également le format que vous préférez.

Author avatar
Jean-Baptiste Nahan

Consultant Expert Web, j'aide les entreprises ayant des difficultés avec leur projet Web (PHP, Symfony, Sylius).

@jbnahan69 | Macintoshplus | Linkedin | JB Dev Labs