Cette réponse est écrite pour montrer à quel point il est trivial de contourner un code de validation utilisateur PHP mal écrit, comment (et à l'aide de quoi) ces attaques fonctionnent et comment remplacer les anciennes fonctions MySQL par une instruction préparée sécurisée - et, en gros, pourquoi les utilisateurs de StackOverflow (qui ont probablement beaucoup de réputation) aboient sur les nouveaux utilisateurs qui posent des questions pour améliorer leur code.
Tout d'abord, n'hésitez pas à créer cette base de données mysql de test (j'ai appelé la mienne prep) :
mysql> create table users(
-> id int(2) primary key auto_increment,
-> userid tinytext,
-> pass tinytext);
Query OK, 0 rows affected (0.05 sec)
mysql> insert into users values(null, 'Fluffeh', 'mypass');
Query OK, 1 row affected (0.04 sec)
mysql> create user 'prepared'@'localhost' identified by 'example';
Query OK, 0 rows affected (0.01 sec)
mysql> grant all privileges on prep.* to 'prepared'@'localhost' with grant option;
Query OK, 0 rows affected (0.00 sec)
Ceci étant fait, nous pouvons passer à notre code PHP.
Supposons que le script suivant soit le processus de vérification d'un administrateur sur un site web (simplifié mais fonctionnel si vous le copiez et l'utilisez pour le tester) :
<?php
if(!empty($_POST['user']))
{
$user=$_POST['user'];
}
else
{
$user='bob';
}
if(!empty($_POST['pass']))
{
$pass=$_POST['pass'];
}
else
{
$pass='bob';
}
$database='prep';
$link=mysql_connect('localhost', 'prepared', 'example');
mysql_select_db($database) or die( "Unable to select database");
$sql="select id, userid, pass from users where userid='$user' and pass='$pass'";
//echo $sql."<br><br>";
$result=mysql_query($sql);
$isAdmin=false;
while ($row = mysql_fetch_assoc($result)) {
echo "My id is ".$row['id']." and my username is ".$row['userid']." and lastly, my password is ".$row['pass']."<br>";
$isAdmin=true;
// We have correctly matched the Username and Password
// Lets give this person full access
}
if($isAdmin)
{
echo "The check passed. We have a verified admin!<br>";
}
else
{
echo "You could not be verified. Please try again...<br>";
}
mysql_close($link);
?>
<form name="exploited" method='post'>
User: <input type='text' name='user'><br>
Pass: <input type='text' name='pass'><br>
<input type='submit'>
</form>
Ça semble assez légitime à première vue.
L'utilisateur doit entrer un login et un mot de passe, non ?
Brillant, ne pas entrer dans ce qui suit :
user: bob
pass: somePass
et le soumettre.
Le résultat est le suivant :
You could not be verified. Please try again...
Super ! Cela fonctionne comme prévu, maintenant essayons le nom d'utilisateur et le mot de passe réels :
user: Fluffeh
pass: mypass
Incroyable ! Hi-fives tout autour, le code a correctement vérifié un administrateur. C'est parfait !
Eh bien, pas vraiment. Disons que l'utilisateur est une petite personne intelligente. Disons que cette personne, c'est moi.
Entrez ce qui suit :
user: bob
pass: n' or 1=1 or 'm=m
Et le résultat est :
The check passed. We have a verified admin!
Félicitations, vous venez de me permettre d'entrer dans votre section super-protégée réservée aux admins en entrant un faux nom d'utilisateur et un faux mot de passe. Sérieusement, si vous ne me croyez pas, créez la base de données avec le code que j'ai fourni, et exécutez ce code PHP - qui à première vue semble vérifier le nom d'utilisateur et le mot de passe plutôt bien.
Donc, en réponse, C'EST POURQUOI ON VOUS CRIE dessus.
Voyons donc ce qui s'est passé, et pourquoi je suis entré dans votre caverne de chauve-souris réservée aux super-administrateurs. J'ai supposé que vous ne faisiez pas attention à vos entrées et que vous les transmettiez directement à la base de données. J'ai construit l'entrée d'une manière qui changerait la requête que vous exécutez réellement. Alors, qu'est-ce que c'était censé être, et qu'est-ce que ça a fini par être ?
select id, userid, pass from users where userid='$user' and pass='$pass'
C'est la requête, mais lorsque nous remplaçons les variables par les entrées réelles que nous avons utilisées, nous obtenons ce qui suit :
select id, userid, pass from users where userid='bob' and pass='n' or 1=1 or 'm=m'
Vous voyez comment j'ai construit mon "mot de passe" pour qu'il ferme d'abord le guillemet simple autour du mot de passe, puis qu'il introduise une toute nouvelle comparaison ? Puis, par sécurité, j'ai ajouté une autre "chaîne" pour que le guillemet simple soit fermé comme prévu dans le code que nous avions initialement.
Cependant, il ne s'agit pas de vous crier dessus maintenant, mais de vous montrer comment rendre votre code plus sûr.
Ok, alors qu'est-ce qui a mal tourné, et comment on peut le réparer ?
Il s'agit d'une attaque classique par injection SQL. L'une des plus simples d'ailleurs. À l'échelle des vecteurs d'attaque, c'est un enfant qui attaque un char d'assaut - et qui gagne.
Alors, comment protéger votre section d'administration sacrée et la rendre agréable et sûre ? La première chose à faire est d'arrêter d'utiliser ces très vieux et dépréciés mysql_*
fonctions. Je sais, vous avez suivi un tutoriel que vous avez trouvé en ligne et ça marche, mais c'est vieux, c'est dépassé et en l'espace de quelques minutes, je l'ai dépassé sans même transpirer.
Maintenant, vous avez la possibilité d'utiliser mysqli_ o AOP . Je suis personnellement un grand fan de PDO, donc je vais utiliser PDO dans le reste de cette réponse. Il y a des avantages et des inconvénients, mais personnellement, je trouve que les avantages l'emportent largement sur les inconvénients. Il est portable sur plusieurs moteurs de bases de données - que vous utilisiez MySQL ou Oracle ou n'importe quoi d'autre - simplement en changeant la chaîne de connexion, il a toutes les fonctionnalités fantaisistes que nous voulons utiliser et il est beau et propre. J'aime la propreté.
Maintenant, regardons à nouveau ce code, cette fois-ci écrit en utilisant un objet PDO :
<?php
if(!empty($_POST['user']))
{
$user=$_POST['user'];
}
else
{
$user='bob';
}
if(!empty($_POST['pass']))
{
$pass=$_POST['pass'];
}
else
{
$pass='bob';
}
$isAdmin=false;
$database='prep';
$pdo=new PDO ('mysql:host=localhost;dbname=prep', 'prepared', 'example');
$sql="select id, userid, pass from users where userid=:user and pass=:password";
$myPDO = $pdo->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
if($myPDO->execute(array(':user' => $user, ':password' => $pass)))
{
while($row=$myPDO->fetch(PDO::FETCH_ASSOC))
{
echo "My id is ".$row['id']." and my username is ".$row['userid']." and lastly, my password is ".$row['pass']."<br>";
$isAdmin=true;
// We have correctly matched the Username and Password
// Lets give this person full access
}
}
if($isAdmin)
{
echo "The check passed. We have a verified admin!<br>";
}
else
{
echo "You could not be verified. Please try again...<br>";
}
?>
<form name="exploited" method='post'>
User: <input type='text' name='user'><br>
Pass: <input type='text' name='pass'><br>
<input type='submit'>
</form>
Les principales différences sont qu'il n'y a plus de mysql_*
fonctions. Tout se fait par l'intermédiaire d'un objet PDO. Ensuite, on utilise une instruction préparée. Maintenant, qu'est-ce qu'une instruction préparée, vous vous demandez ? C'est un moyen de dire à la base de données avant d'exécuter une requête, quelle est la requête que nous allons exécuter. Dans ce cas, nous disons à la base de données : "Bonjour, je vais exécuter une commande select voulant id, userid et pass de la table users où le userid est une variable et le pass est aussi une variable.".
Ensuite, dans l'instruction d'exécution, nous transmettons à la base de données un tableau contenant toutes les variables qu'elle attend maintenant.
Les résultats sont fantastiques. Essayons à nouveau ces combinaisons de nom d'utilisateur et de mot de passe :
user: bob
pass: somePass
L'utilisateur n'a pas été vérifié. Génial.
Pourquoi pas :
user: Fluffeh
pass: mypass
Oh, je me suis juste un peu excité, ça a marché : Le chèque est passé. Nous avons un administrateur vérifié !
Maintenant, essayons les données qu'un petit malin entrerait pour essayer de passer notre petit système de vérification :
user: bob
pass: n' or 1=1 or 'm=m
Cette fois, nous obtenons ce qui suit :
You could not be verified. Please try again...
C'est pourquoi on vous crie dessus lorsque vous posez des questions - c'est parce que les gens peuvent voir que votre code peut être contourné sans même essayer. S'il vous plaît, utilisez cette question et cette réponse pour améliorer votre code, pour le rendre plus sûr et pour utiliser des fonctions qui sont à jour.
Enfin, cela ne veut pas dire que ce code est PARFAIT. Il y a beaucoup d'autres choses que vous pourriez faire pour l'améliorer, utiliser des mots de passe hachés par exemple, vous assurer que lorsque vous stockez des informations sensibles dans la base de données, vous ne les stockez pas en texte clair, avoir plusieurs niveaux de vérification - mais vraiment, si vous changez simplement votre vieux code sujet aux injections par celui-ci, le fait que vous soyez arrivé jusqu'ici et que vous continuiez à lire me donne l'espoir que vous ne vous contenterez pas de mettre en œuvre ce type de code lorsque vous écrirez vos sites Web et vos applications, mais que vous ferez des recherches sur les autres points que je viens de mentionner, et plus encore. Écrivez le meilleur code possible, pas le code le plus basique qui fonctionne à peine.
3 votes
L'erreur doit être comme : Erreur fatale : Unecaught Error : Appel à une fonction non définie mysql_connect() ...
48 votes
Le fait qu'ils soient dépréciés est une raison suffisante pour les éviter.