Est-ce que htmlspecialchars et mysql_real_escape_string protègent mon code PHP de l'injection?

116

Plus tôt dans la journée, une question a été posée concernant les stratégies de validation des entrées dans les applications Web .

La première réponse, au moment de la rédaction de cet article, suggère d' PHPutiliser simplement htmlspecialcharset mysql_real_escape_string.

Ma question est la suivante: est-ce toujours suffisant? Y a-t-il plus que nous devrions savoir? Où ces fonctions se décomposent-elles?

Cheekysoft
la source

Réponses:

241

Quand il s'agit de requêtes de base de données, essayez toujours d'utiliser des requêtes paramétrées préparées. Les bibliothèques mysqliet PDOprennent en charge cela. C'est infiniment plus sûr que d'utiliser des fonctions d'échappement telles que mysql_real_escape_string.

Oui, mysql_real_escape_stringc'est en fait juste une fonction d'échappement de chaîne. Ce n'est pas une solution miracle. Tout ce qu'il fera est d'échapper aux caractères dangereux afin qu'ils puissent être utilisés en toute sécurité dans une seule chaîne de requête. Cependant, si vous ne nettoyez pas vos entrées au préalable, vous serez vulnérable à certains vecteurs d'attaque.

Imaginez le SQL suivant:

$result = "SELECT fields FROM table WHERE id = ".mysql_real_escape_string($_POST['id']);

Vous devriez être en mesure de voir que cela est vulnérable à l'exploitation.
Imaginez que le idparamètre contienne le vecteur d'attaque commun:

1 OR 1=1

Il n'y a pas de caractères risqués à encoder, il passera donc directement à travers le filtre qui s'échappe. En nous quittant:

SELECT fields FROM table WHERE id= 1 OR 1=1

Ce qui est un joli vecteur d'injection SQL et permettrait à l'attaquant de renvoyer toutes les lignes. Ou

1 or is_admin=1 order by id limit 1

qui produit

SELECT fields FROM table WHERE id=1 or is_admin=1 order by id limit 1

Ce qui permet à l'attaquant de renvoyer les détails du premier administrateur dans cet exemple complètement fictif.

Bien que ces fonctions soient utiles, elles doivent être utilisées avec précaution. Vous devez vous assurer que toutes les entrées Web sont validées dans une certaine mesure. Dans ce cas, nous voyons que nous pouvons être exploités car nous n'avons pas vérifié qu'une variable que nous utilisions comme nombre, était en fait numérique. En PHP, vous devriez largement utiliser un ensemble de fonctions pour vérifier que les entrées sont des entiers, des flottants, des caractères alphanumériques, etc. Mais quand il s'agit de SQL, faites surtout attention à la valeur de l'instruction préparée. Le code ci-dessus aurait été sécurisé s'il s'agissait d'une instruction préparée, car les fonctions de base de données auraient su que ce 1 OR 1=1n'est pas un littéral valide.

Quant à htmlspecialchars(). C'est un champ de mines en soi.

Il y a un vrai problème en PHP en ce qu'il a toute une sélection de différentes fonctions d'échappement liées au HTML, et aucune indication claire sur exactement quelles fonctions font quoi.

Premièrement, si vous êtes à l'intérieur d'une balise HTML, vous avez de vrais problèmes. Regarder

echo '<img src= "' . htmlspecialchars($_GET['imagesrc']) . '" />';

Nous sommes déjà dans une balise HTML, nous n'avons donc pas besoin de <ou> de faire quoi que ce soit de dangereux. Notre vecteur d'attaque pourrait êtrejavascript:alert(document.cookie)

Le HTML résultant ressemble maintenant à

<img src= "javascript:alert(document.cookie)" />

L'attaque passe directement.

Ça s'empire. Pourquoi? car htmlspecialchars(lorsqu'il est appelé de cette façon) n'encode que les guillemets doubles et non les simples. Donc si nous avions

echo "<img src= '" . htmlspecialchars($_GET['imagesrc']) . ". />";

Notre attaquant maléfique peut désormais injecter de tout nouveaux paramètres

pic.png' onclick='location.href=xxx' onmouseover='...

nous donne

<img src='pic.png' onclick='location.href=xxx' onmouseover='...' />

Dans ces cas, il n'y a pas de solution miracle, il vous suffit de santiser vous-même l'entrée. Si vous essayez de filtrer les mauvais caractères, vous échouerez sûrement. Adoptez une approche de liste blanche et ne laissez passer que les caractères qui sont bons. Regardez la feuille de triche XSS pour des exemples sur la diversité des vecteurs

Même si vous utilisez en htmlspecialchars($string)dehors des balises HTML, vous êtes toujours vulnérable aux vecteurs d'attaque de jeux de caractères multi-octets.

Le plus efficace que vous puissiez être est d'utiliser la combinaison de mb_convert_encoding et htmlentities comme suit.

$str = mb_convert_encoding($str, 'UTF-8', 'UTF-8');
$str = htmlentities($str, ENT_QUOTES, 'UTF-8');

Même cela laisse IE6 vulnérable, en raison de la façon dont il gère UTF. Cependant, vous pouvez revenir à un encodage plus limité, tel que ISO-8859-1, jusqu'à ce que l'utilisation d'IE6 diminue.

Pour une étude plus approfondie des problèmes multi-octets, voir https://stackoverflow.com/a/12118602/1820

Cheekysoft
la source
24
La seule chose qui a manqué ici, c'est que le premier exemple pour la requête DB ... un simple intval () résoudrait l'injection. Utilisez toujours intval () à la place de mysqlescape ... () lorsque vous avez besoin d'un nombre et non d'une chaîne.
Robert K
11
et rappelez-vous que l'utilisation de requêtes paramétrées vous permettra de toujours avoir des données traitées comme des données et non comme du code. Utilisez une bibliothèque telle que PDO et utilisez autant que possible des requêtes paramétrées.
Cheekysoft
9
Deux remarques: 1. Dans le premier exemple, vous seriez en sécurité si vous mettez également des guillemets autour du paramètre, comme $result = "SELECT fields FROM table WHERE id = '".mysql_real_escape_string($_POST['id'])."'";2. Dans le second cas (attribut contenant une URL), il n'y a aucune utilité pour htmlspecialchars; dans ces cas, vous devez encoder l'entrée en utilisant un schéma d'encodage URL, par exemple en utilisant rawurlencode. De cette façon, un utilisateur ne peut pas insérer javascript:et al.
Marcel Korpel
7
«Htmlspecialchars n'encode que des guillemets doubles et non des simples»: ce n'est pas vrai, cela dépend des indicateurs en cours de définition, voir ses paramètres .
Marcel Korpel
2
Cela doit être mis en gras: Take a whitelist approach and only let through the chars which are good.une liste noire manquera toujours quelque chose. +1
Jo Smo
10

En plus de l'excellente réponse de Cheekysoft:

  • Oui, ils vous protégeront, mais uniquement s'ils sont utilisés de manière absolument correcte. Utilisez-les de manière incorrecte et vous serez toujours vulnérable et pourriez avoir d'autres problèmes (par exemple, la corruption des données)
  • Veuillez utiliser des requêtes paramétrées à la place (comme indiqué ci-dessus). Vous pouvez les utiliser par exemple via PDO ou via un wrapper comme PEAR DB
  • Assurez-vous que magic_quotes_gpc et magic_quotes_runtime sont désactivés à tout moment et ne sont jamais activés accidentellement, même pas brièvement. Il s'agit d'une tentative précoce et profondément malavisée des développeurs PHP pour éviter les problèmes de sécurité (qui détruisent les données)

Il n'y a pas vraiment de solution miracle pour empêcher l'injection HTML (par exemple, les scripts intersites), mais vous pourrez peut-être y parvenir plus facilement si vous utilisez une bibliothèque ou un système de modèles pour générer du HTML. Lisez la documentation pour cela pour savoir comment échapper aux choses de manière appropriée.

En HTML, les choses doivent être échappées différemment selon le contexte. Cela est particulièrement vrai des chaînes placées dans Javascript.

MarkR
la source
3

Je serais certainement d'accord avec les articles ci-dessus, mais j'ai une petite chose à ajouter en réponse à la réponse de Cheekysoft, en particulier:

Quand il s'agit de requêtes de base de données, essayez toujours d'utiliser des requêtes paramétrées préparées. Les bibliothèques mysqli et PDO le supportent. C'est infiniment plus sûr que d'utiliser des fonctions d'échappement telles que mysql_real_escape_string.

Oui, mysql_real_escape_string n'est en fait qu'une fonction d'échappement de chaîne. Ce n'est pas une solution miracle. Tout ce qu'il fera est d'échapper aux caractères dangereux afin qu'ils puissent être utilisés en toute sécurité dans une seule chaîne de requête. Cependant, si vous ne nettoyez pas vos entrées au préalable, vous serez vulnérable à certains vecteurs d'attaque.

Imaginez le SQL suivant:

$ result = "CHOISIR les champs FROM table WHERE id =" .mysql_real_escape_string ($ _ POST ['id']);

Vous devriez être en mesure de voir que cela est vulnérable à l'exploitation. Imaginez que le paramètre id contienne le vecteur d'attaque commun:

1 OU 1 = 1

Il n'y a pas de caractères risqués à encoder, il passera donc directement à travers le filtre qui s'échappe. En nous quittant:

SELECT champs FROM table WHERE id = 1 OU 1 = 1

J'ai codé une petite fonction rapide que j'ai mise dans ma classe de base de données qui supprimera tout ce qui n'est pas un nombre. Il utilise preg_replace, donc il y a probablement une fonction un peu plus optimisée, mais cela fonctionne à la rigueur ...

function Numbers($input) {
  $input = preg_replace("/[^0-9]/","", $input);
  if($input == '') $input = 0;
  return $input;
}

Donc au lieu d'utiliser

$ result = "CHOISIR les champs FROM table WHERE id =" .mysqlrealescapestring ("1 OR 1 = 1");

j'utiliserais

$ result = "CHOISIR les champs FROM table WHERE id =" .Numbers ("1 OR 1 = 1");

et il exécuterait en toute sécurité la requête

CHOISIR les champs FROM table WHERE id = 111

Bien sûr, cela l'a juste empêché d'afficher la bonne ligne, mais je ne pense pas que ce soit un gros problème pour quiconque essaie d'injecter SQL dans votre site;)

BrillantHiver
la source
1
Parfait! C'est exactement le type de désinfection dont vous avez besoin. Le code initial a échoué car il n'a pas validé qu'un nombre était numérique. Votre code le fait. vous devez appeler Numbers () sur toutes les variables à usage entier dont les valeurs proviennent de l'extérieur de la base de code.
Cheekysoft le
1
Il est à noter que intval () fonctionnera parfaitement pour cela, car PHP contraint automatiquement les entiers en chaînes pour vous.
Adam Ernst
11
Je préfère intval. Il tourne 1abc2 à 1, pas 12.
jmucchiello
1
intval est mieux, surtout sur ID. La plupart du temps, s'il a été corrompu, c'est comme ci-dessus, 1 ou 1 = 1. Vous ne devriez vraiment pas divulguer l'identité des autres. Donc intval renverra l'ID correct. Après cela, vous devez vérifier si les valeurs d'origine et nettoyées sont les mêmes. C'est un excellent moyen non seulement d'arrêter les attaques, mais aussi de trouver les attaquants.
triunenature
2
Une ligne incorrecte serait désastreuse si vous affichez des données personnelles, vous verriez les informations d'un autre utilisateur! au lieu de cela, il serait préférable de vérifierreturn preg_match('/^[0-9]+$/',$input) ? $input : 0;
Frank Forte
2

Les contextes sont une pièce importante de ce puzzle. Quelqu'un qui envoie "1 OR 1 = 1" comme ID ne pose pas de problème si vous citez tous les arguments de votre requête:

SELECT fields FROM table WHERE id='".mysql_real_escape_string($_GET['id'])."'"

Ce qui se traduit par:

SELECT fields FROM table WHERE id='1 OR 1=1'

ce qui est inefficace. Puisque vous échappez à la chaîne, l'entrée ne peut pas sortir du contexte de chaîne. J'ai testé cela jusqu'à la version 5.0.45 de MySQL, et l'utilisation d'un contexte de chaîne pour une colonne entière ne pose aucun problème.

Lucas Oman
la source
15
puis je vais commencer mon vecteur d'attaque avec le caractère multi-octets 0xbf27 qui dans votre base de données latin1 sera converti par la fonction de filtre en 0xbf5c27 - qui est un caractère multi-octets suivi d'un guillemet simple.
Cheekysoft le
8
Essayez de ne pas vous protéger contre un seul vecteur d'attaque connu. Vous finirez par courir après votre queue jusqu'à la fin des temps en appliquant patch après patch à votre code. Prendre du recul et examiner les cas généraux passera à un code plus sûr et à un meilleur état d'esprit axé sur la sécurité.
Cheekysoft le
Je suis d'accord; idéalement, OP utilisera des déclarations préparées.
Lucas Oman
1
Bien que la citation des arguments suggérés par cet article ne soit pas infaillible, elle atténuera la plupart des attaques courantes de type 1 OR 1 = 1, donc cela mérite d'être mentionné.
Night Owl
2
$result = "SELECT fields FROM table WHERE id = ".(INT) $_GET['id'];

Fonctionne bien, encore mieux sur les systèmes 64 bits. Méfiez-vous des limites de vos systèmes pour traiter de grands nombres, mais pour les identifiants de base de données, cela fonctionne très bien 99% du temps.

Vous devriez également utiliser une seule fonction / méthode pour nettoyer vos valeurs. Même si cette fonction n'est qu'un wrapper pour mysql_real_escape_string (). Pourquoi? Parce qu'un jour, lorsqu'un exploit de votre méthode préférée de nettoyage des données est trouvé, vous n'aurez à le mettre à jour qu'à un seul endroit, plutôt qu'une recherche et un remplacement à l'échelle du système.

cnizzardini
la source
-3

pourquoi, oh POURQUOI, n'incluriez- vous pas des guillemets autour de l'entrée utilisateur dans votre instruction SQL? semble assez idiot de ne pas le faire! inclure des guillemets dans votre instruction SQL rendrait "1 ou 1 = 1" une tentative infructueuse, non?

alors maintenant, vous direz, "et si l'utilisateur inclut un devis (ou des guillemets doubles) dans l'entrée?"

Eh bien, solution facile pour cela: supprimez simplement les citations entrées par l'utilisateur. par exemple: input =~ s/'//g;. maintenant, il me semble de toute façon, que l'entrée de l'utilisateur serait sécurisée ...

Jarett L
la source
"Pourquoi, oh POURQUOI, n'incluriez-vous pas de guillemets autour de l'entrée utilisateur dans votre instruction SQL?" - La question ne dit rien sur le fait de ne pas citer l'entrée utilisateur.
Quentin
1
"bien, solution facile pour ça" - Terrible solution pour ça. Cela jette des données. La solution mentionnée dans la question elle-même est une meilleure approche.
Quentin
Bien que je convienne que la question ne traite pas de la citation de l'entrée de l'utilisateur, il semble toujours de ne pas citer l'entrée. et, je préfère lancer des données plutôt que de saisir de mauvaises données. en général, lors d'une attaque par injection, vous ne voulez PAS de ces données de toute façon ... non?
Jarett L
"Bien que je convienne que la question ne traite pas de la citation de l'entrée de l'utilisateur, il semble toujours de ne pas citer l'entrée." - Non, non. La question ne le démontre pas d'une manière ou d'une autre.
Quentin
1
@JarettL Soit vous habituer à utiliser des instructions préparées, soit vous habituer à Bobby Tables détruisant vos données tous les mardis . Le SQL paramétré est le meilleur moyen de vous protéger contre l'injection SQL. Vous n'avez pas besoin de faire des "vérifications d'injection SQL" si vous utilisez une instruction préparée. Ils sont extrêmement faciles à implémenter (et à mon avis, rendent le code BEAUCOUP plus facile à lire), protègent de diverses idiosyncrasies de concaténation de chaînes et d'injection SQL, et mieux encore, vous n'avez pas à réinventer la roue pour l'implémenter .
Siyual