Validation des e-mails en PHP
Ce script est-il suffisant pour valider l'entrée de l'e-mail de l'utilisateur?
<?php
//1 DATABASE CONNECTION
$dbHost = "HOST"; $dbUser = "USER";
$dbPassword = "PASSWORD"; $dbName = "DATABASE";
try {
$dsn = "mysql:host=" . $dbHost . ";dbname=" . $dbName; $pdo = new PDO($dsn, $dbUser, $dbPassword); $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch(PDOException $e) {
echo "DB Connection Failed: " . $e->getMessage(); exit(0); } //1 END //2 ADD EMAIL TO DATABASE //set date and time date_default_timezone_set('America/Los_Angeles'); $timestamp = strtotime('NOW');
$dateTime = date('Ymd-His', $timestamp);
//variable to store ipv4 address
$userIP4 = gethostbyname($_SERVER['REMOTE_ADDR']);
//storing ip6 could be something like: "bin2hex(inet_pton($_SERVER['REMOTE_ADDR']));" but I couldn't figure out if the output was correct, because it looked nothing like an ipv6 address..... if(filter_var($userIP4, FILTER_VALIDATE_IP)) {
//yes it's valid IPv4
if($_SERVER['REQUEST_METHOD'] == 'POST') { $email = htmlspecialchars($_POST['email']); //convert special characters to HTML entities (&,",<,>) $Temail = trim($email); //trim spaces on ends //allow international characters if(preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$^/", $Temail)) { //prevents invalid email addresses header("Location: invalid.html"); exit (0); } else { //Check Email Domain MX Record $email_host = strtolower(substr(strrchr($Temail, "@"), 1)); if (!checkdnsrr($email_host, "MX")) {
header("Location: invalid.html");
exit (0);
} else {
//Prevent users from inputting a specific domain...
$notallowed = [ 'mydomain.com', ]; $parts = explode('@', $Temail); //Separate string by @ characters (there should be only one) $domain = array_pop($parts); //Remove and return the last part, which should be the domain if ( ! in_array($domain, $notallowed)) { //checks database to make sure the email is not a duplicate $stmt1 = $pdo->prepare("SELECT * FROM emailTable WHERE email=?"); $stmt1->execute([$Temail]); $user = $stmt1->fetch(); if($user) {
//prevents adding a duplicate email
header("Location: duplicate.html");
exit (0);
} else {
//generate Activation code
$Acode = md5(time().$Temail);
//send verification email
$emailfrom = '[email protected]'; $fromname = 'MY NAME';
$subject = 'Confirm Your Email Subscription'; $emailbody = "
<html>
<body style='background-color: #000; padding: 15px;'>
<table style='background-color: #222;'>
<tr style='background-color: #333; padding: 15px; font-size: 1.3rem;'>
<td><h2 style='color: #FFF;' align='center'>Please Verify Subscription</h2></td>
</tr>
<tr>
<td style='color: #FFF; font-size: 1.1rem;' align='center'>
<br/>
<br/>
If you didn't sign up for my email list, simply delete this message. You will not be added unless you push the button below.
<br/>
<br/>
</td>
</tr>
<tr>
<td style='color: #FFF; font-size: 1.3rem;' align='center'>
<button style='background-color: #000; width: 6rem; height: 2rem;'><a href='https://www.MYDOMAIN.com/verify.php?acode=$Acode' style='color: #F00; text-decoration: none; font-size:1rem;'>VERIFY</a></button> <br/> <br/> </td> </tr> <tr> <td style='color: #FFF; font-size: 1.1rem;' align='center'> <font style='font-size:0.8rem;'>This email was automatically generated from a mailbox that is not monitored.</font> </td> </tr> </table> </body> </html>"; $headers = "Reply-To: MY NAME <[email protected]>\r\n";
$headers .= "Return-Path: MY NAME <[email protected]>\r\n"; $headers .= "From: MY NAME <[email protected]>\r\n";
$headers .= "MIME-Version: 1.0\r\n"; $headers .= "Content-type: text/html; charset=UTF-8\r\n";
$headers .= "X-Priority: 3\r\n"; $headers .= "X-Mailer: PHP". phpversion() ."\r\n" ;
$params = '-f ' . $emailfrom;
$send = mail($Temail, $subject, $emailbody, $headers, $params); // $send should be TRUE if the mail function is called correctly if($send) {
//add the new email and other data to the database
$sql = "INSERT INTO emailTable (IP4, datetime, email, acode) VALUES (:IP4, :datetime, :email, :acode)"; $stmt2 = $pdo->prepare($sql);
$stmt2->execute(['IP4' => $userIP4, 'datetime' => $dateTime, 'email' => $Temail, 'acode' => $Acode]); $userIP4 = "";
$dateTime = ""; $Temail = "";
$Acode = "";
header("Location: success.html");
exit (0);
} else {
header("Location: invalid.html");
exit (0);
}
}
} else {
header("Location: notallowed.html");
exit (0);
}
}
}
} else {
header("Location: invalid.html");
exit (0);
}
} else {
header("Location: invalid.html");
exit (0);
}
//2 END
?>
Menaces de sécurité à l'esprit:
1. Injections SQL !!! --- Solutions: Instructions préparées (PDO), utilisant uniquement UTF-8, et incluant "$ bpdo-> setAttribute (PDO :: ATTR_EMULATE_PREPARES, false);" dans la connexion à la base de données
2. Attaques XSS !!! --- Solutions: htmlspecialchars (), Content-Security Policy (placé dans htaccess):
<FilesMatch "\.(html|php)$">
Header set Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: 'unsafe-inline'; media-src 'self' data: 'unsafe-inline'; connect-src 'self';"
</FilesMatch>
3. Attaques de commande OS !!! --- Solutions: Striping des espaces (pas nécessaire avec les e-mails) , validation par rapport à une liste blanche de valeurs autorisées.
4. Attaques DOS !!! --- Solution: aucune mise en œuvre. Je ne sais pas si des précautions supplémentaires sont nécessaires, car il n'y a pas de possibilité de connexion sur mon site Web.
5. Injection de courrier électronique PHP !!! --- Solution: une expression régulière (celle que j'ai est principalement conçue pour permettre des caractères internationaux).
De plus, j'utilise un certificat SSL, SiteLock Security-Essential, CloudFlare CDN et j'ai implémenté une politique DMARC dans mon DNS (quelque chose que je peaufinerai dans un avenir prévisible).
Réponses
Tout d'abord, quel que soit le problème de sécurité réel ou imaginaire, ce code est une douleur à l'œil . Il est presque impossible d'en comprendre l'essentiel et de répondre à la question réelle car le code est constamment hors de vue et la grande quantité de HTML gêne au milieu du soi-disant code de vérification des e-mails. Des vérifications inutiles ajoutent également à cela.
Vous devriez vraiment réécrire votre code d'abord, afin de le rendre lisible. Après tout, c'est aussi une sorte de problème de sécurité - dans un tel désert, il est facile d'ignorer un vrai problème. Donnez de l'amour à votre code :
- déplacez la connexion à la base de données dans un fichier séparé , puis incluez-la simplement. Au fait, voici comment le faire correctement . À l'heure actuelle, votre connexion PDO est un problème de sécurité car révéler le message d'erreur système au monde extérieur n'est pas mineur
- créer une fonction pour envoyer des e-mails, mettre toutes ces
$headers .= "From: MY NAME <[email protected]>\r\n";affaires là-bas. puis placez la fonction elle-même dans un fichier séparé, puis incluez-la simplement. - déplacez le code pour envoyer l'e-mail réel dans une fonction et placez cette fonction au bas du code. Cela ne détournera donc pas le lecteur de l'algorithme principal.
- débarrassez-vous du code inutile. Vérifier REMOTE_ADDR n'a aucun sens, il n'y a aucune situation où il serait invalide. htmlspecialchars est également inutile ici. et effacer vos variables, c'est-
$userIP4 = "";à- dire aussi - débarrassez-vous de cet escabeau d'un code. Compte tenu de vos conditions, arrêtez l'exécution de toute façon - pourquoi ne pas simplement tester le résultat négatif et arrêter l'exécution?
Donc au lieu de
if (condition) {
if (condition2) {
do something;
} else {
display error;
die;
}
} else {
display error;
die;
}
Ecrivez
if (!condition) {
display error;
die;
}
if (!condition2) {
display error;
die;
}
do something;
Une fois votre code adapté à l'examen, vous pouvez poser une nouvelle question concernant vos problèmes de sécurité. De loin, j'ai pu en repérer au moins un.
Divulgation d'informations inutiles
Ne fais pas ça:
$headers .= "X-Mailer: PHP". phpversion() ."\r\n" ;
Cela apparaîtra dans les en-têtes de courrier et sera visible par le destinataire. La version de PHP que vous utilisez n'est pas l'affaire de personne. Si vous utilisez une version de PHP qui présente des vulnérabilités, cela fournit aux pirates un aperçu de vos systèmes pour un exploit sur mesure. Ne copiez pas le code sans en comprendre les implications ...
IPv4 et IPv6
Je ne comprends pas l'intérêt de gethostbyname. Vous avez déjà l'adresse IP, vous pouvez donc la stocker.
Êtes-vous sûr que votre serveur n'est pas accessible sur Internet via IPv6? Vous voudrez peut-être tester votre site sur IPv6 en ajoutant une entrée à votre configuration DNS, par exemple ipv6.yoursite.com avec un enregistrement AAAA et aucun enregistrement A.
Randomisation
Le code de vérification n'est pas vraiment aléatoire :
$Acode = md5(time().$Temail);
time - Retourne l'horodatage Unix actuel
Quelqu'un (un hacker) qui connaît votre formule pourra forcer brutalement le code de vérification car il suit un modèle prévisible. Vous avez de nombreuses meilleures options pour générer des chaînes raisonnablement aléatoires.
Divers
Je réorganiserais probablement du code, par exemple enregistrer l'enregistrement dans la base de données avant d'envoyer le courrier. Si la base de données plante pour une raison quelconque ou si la connexion est perdue, vous avez déjà envoyé un e-mail avec un code de vérification qui n'est enregistré nulle part et ne fonctionnera pas - ce qui est déroutant pour l'utilisateur. Si l'enregistrement n'a pas été enregistré pour une raison quelconque, informez l'utilisateur qu'il y a eu une erreur (et informez-vous également), et ne continuez pas.
L'envoi du courrier est probablement moins susceptible de se bloquer, car il est généralement dirigé vers une file d'attente de courrier pour être géré par le MTA sur votre serveur.
Il est possible que le courrier n'arrive pas ou ait été jeté ou piégé par spam, il devrait donc être possible pour l'utilisateur de demander un nouveau code après un certain temps.
Concernant la connexion à la base de données et la gestion des erreurs, veuillez lire cette réponse . Vous ne devez jamais révéler les détails des erreurs générées par le système à vos utilisateurs finaux - ces détails sont pour vous et pour personne d'autre. Je recommande un
requireappel, mais pas avant que la soumission de l'utilisateur ne soit qualifiée pour son utilisation.Il n'y a absolument aucune raison qui devrait avoir besoin de muter ou de nettoyer l'adresse e-mail entrante. Vous aimerez peut-être les espaces
trim(), mais honnêtement, qui va réellement faire l'erreur d'ajouter un espace voyou? Je n'ai jamais ... jamais.Par souci de cohérence, j'écris toujours mes branches conditionnelles négatives / échouées / fausses avant les branches positives / réussies / vraies. De cette façon, vous (ou d'autres développeurs) saurez que plus le script progresse lentement, plus le flux a réussi et des ressources supplémentaires peuvent être initialisées / déclarées.
Ne générez pas l'horodatage de la ligne db en php. Vous n'avez même pas besoin de le mentionner dans votre sql. Définissez votre
emailTablededatetimecolonneDEFAULTàCURRENT_TIMESTAMP.https://stackoverflow.com/q/168736/2943403Si l'attente est d'autoriser les caractères multi-octets dans les adresses e-mail (
//allow international characters), votre regex n'a pas l'uindicateur nicode. Cela dit, je ne recommande pas d'utiliser regex pour essayer d'analyser / valider une adresse e-mail, car à mesure que votre modèle de validation améliore sa précision, la lisibilité et la maintenabilité s'effondrent. je recommandefilter_var()https://stackoverflow.com/q/12026842/2943403$email_hostcontient déjà le domaine, alors ne recommencez pas l'opération avecexplode()/array_pop().SELECT * FROM emailTable WHERE email=?demande trop de données. Vous n'avez besoin queCOUNT()des données de la ligne, pas des données. De cette façon, vous ne récupérez que le nombre (qui sera zéro ou un), donc la condition est très simple et lisible.Si vous souhaitez SÉCHER les lignes "header & die", vous pouvez créer une petite fonction d'assistance qui reçoit le texte de Location, puis appelle le
header(), puisdie()s.Je recommanderais d'utiliser la syntaxe
<<<HTML ... HTML;(HEREDOC) afin que vous puissiez utiliser des guillemets doubles dans votre balisage et vos variables en ligne. Vous pouvez également utilisersprintf(), mais c'est moins convaincant avec une seule variable.Puisque lesje recommande de créer un tableau de lignes$headerslignes sont toutes délimitées par\r\n,, puis decréerimplode()\r\navecpour être plus sec. Le quatrième paramètre demail()est les données d'en-tête qui peuvent recevoir un tableau.Je ne recommande pas le natif
mail(). J'intègre toujours PHPMailer dans tous mes projets pour la facilité d'utilisation et la fonctionnalité.Je suis préoccupé par l'ambiguïté si
invalid.html. En cas d'mail()échec, l'utilisateur ne saura pas s'il y a quelque chose qu'il peut réparer ou non.$userIP4 = ""; $dateTime = ""; $Temail = ""; $Acode = "";tout cela est un non-sens inutile, omettez simplement tout cela.Passez en revue votre script et recherchez des variables à usage unique. Lorsque vous voyez une variable déclarée utilisée une seule fois, vous n'avez pas besoin de la déclarer.