Validación de correo electrónico en PHP
¿Es este script suficiente para validar la entrada de correo electrónico del usuario?
<?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
?>
Amenazas de seguridad en mente:
1. Inyecciones SQL !!! --- Soluciones: Declaraciones preparadas (PDO), usando solo UTF-8, e incluyendo "$ bpdo-> setAttribute (PDO :: ATTR_EMULATE_PREPARES, false);" en la conexión a la base de datos
2. Ataques XSS !!! --- Soluciones: htmlspecialchars (), Política de seguridad de contenido (colocada en 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. Ataques de comandos del sistema operativo !!! --- Soluciones: Franjas de espacios en blanco (no es necesario con correos electrónicos) , validando contra una lista blanca de valores permitidos.
4. Ataques DOS !!! --- Solución: Ninguna implementada. No estoy seguro de si es necesaria alguna precaución adicional, ya que no hay posibilidades de inicio de sesión en mi sitio web.
5. Inyección de correo electrónico PHP !!! --- Solución: Una expresión regular (la que tengo está diseñada principalmente para permitir caracteres internacionales).
Además, utilizo un certificado SSL, SiteLock Security- Essential, CloudFlare CDN y he implementado una política DMARC en mi DNS (algo que estaré afinando en el futuro previsible).
Respuestas
En primer lugar, independientemente de cualquier problema de seguridad real o imaginario, este código es un dolor de cabeza . Es casi imposible entender la esencia y responder la pregunta real porque el código se pierde de vista constantemente y la gran cantidad de HTML se interpone en medio del supuesto código de verificación del correo electrónico. Las verificaciones inútiles también se suman a eso.
Realmente deberías reescribir tu código primero, para que sea legible. Después de todo, también es una especie de problema de seguridad: en un lugar tan salvaje es fácil pasar por alto un problema real. Dale un poco de amor a tu código :
- mueva la conexión de la base de datos a un archivo separado y luego inclúyalo. Por cierto, aquí se explica cómo hacerlo correctamente . En este momento, su conexión PDO es un problema de seguridad porque revelar el mensaje de error del sistema al mundo exterior no es menor
- crear una función para enviar correos electrónicos, poner todo este
$headers .= "From: MY NAME <[email protected]>\r\n";negocio allí. luego coloque la función en un archivo separado y luego inclúyala. - mueva el código para enviar el correo electrónico real a una función y coloque esta función en la parte inferior del código. Por lo tanto, no distraerá al lector del algoritmo principal.
- deshacerse del código inútil. Marcar REMOTE_ADDR no tiene sentido, no hay ninguna situación en la que no sea válido. htmlspecialchars también es inútil aquí. y borrar sus variables, es decir
$userIP4 = "";, también - deshacerse de esa escalera de un código. Dadas sus condiciones, detener la ejecución de todos modos, ¿por qué no simplemente probar el resultado negativo y detener la ejecución?
Entonces en lugar de
if (condition) {
if (condition2) {
do something;
} else {
display error;
die;
}
} else {
display error;
die;
}
solo escribe
if (!condition) {
display error;
die;
}
if (!condition2) {
display error;
die;
}
do something;
Después de hacer que su código sea adecuado para la revisión, puede hacer una nueva pregunta sobre sus preocupaciones de seguridad. De lejos, pude detectar al menos uno.
Divulgación de información innecesaria
No hagas esto:
$headers .= "X-Mailer: PHP". phpversion() ."\r\n" ;
Esto se mostrará en los encabezados del correo y será visible para el destinatario. No es asunto de nadie qué versión de PHP está ejecutando. En caso de que esté ejecutando una versión de PHP que tiene vulnerabilidades, esto proporciona a los piratas informáticos información sobre sus sistemas para un exploit personalizado. Simplemente no copie y pegue el código sin comprender las implicaciones ...
IPv4 e IPv6
No entiendo el punto de gethostbyname. Ya tienes la dirección IP, por lo que puedes almacenarla.
¿Está seguro de que no se puede acceder a su servidor en Internet a través de IPv6? Es posible que desee probar su sitio a través de IPv6 agregando una entrada a su configuración de DNS, por ejemplo, ipv6.yoursite.com con un registro AAAA y ningún registro A.
Aleatorización
El código de verificación no es realmente aleatorio :
$Acode = md5(time().$Temail);
time - Devuelve la marca de tiempo actual de Unix
Alguien (un hacker) que conozca su fórmula podrá aplicar fuerza bruta al código de verificación porque sigue un patrón predecible. Tiene muchas mejores opciones para generar cadenas razonablemente aleatorias.
Misc
Probablemente reordenaría algún código, por ejemplo, guardaría el registro en la base de datos antes de enviar el correo. Si la base de datos falla por algún motivo o se pierde la conexión, ya ha enviado un correo electrónico con un código de verificación que no está registrado en ninguna parte y que no funcionará, lo cual es confuso para el usuario. Si el registro no se guardó por alguna razón, notifique al usuario que hubo un error (y notifíquese usted mismo también) y no continúe.
Es menos probable que se bloquee el envío del correo, ya que normalmente va a una cola de correo para que lo maneje el MTA en su servidor.
Es posible que el correo no llegue o haya sido descartado o atrapado en spam, por lo que el usuario debería poder solicitar un nuevo código después de un tiempo.
Con respecto a la conexión de base de datos y el manejo de errores, lea esta respuesta . Nunca debe revelar los detalles de los errores generados por el sistema a sus usuarios finales; estos detalles son para usted y para nadie más. Recomiendo una
requirellamada, pero no antes de que el envío del usuario califique para su uso.No hay absolutamente ninguna razón por la que deba modificar o desinfectar la dirección de correo electrónico entrante. Puede que te gusten los espacios en blanco
trim(), pero, sinceramente, ¿quién va a cometer el error de agregar un espacio deshonesto? Yo nunca ... nunca.Para mantener la coherencia, siempre escribo mis ramas condicionales negativas / fallidas / falsas antes de las ramas positivas / exitosas / verdaderas. De esta manera, usted (u otros desarrolladores) sabrán que cuanto más lento progresa el script, más exitoso ha sido el flujo y se pueden inicializar / declarar recursos adicionales.
No genere la marca de tiempo para la fila db en php. Ni siquiera necesita mencionarlo en su sql. Configurar su
emailTable'sdatetimecolumna paraDEFAULTaCURRENT_TIMESTAMP. https://stackoverflow.com/q/168736/2943403Si la expectativa es permitir caracteres multibyte en direcciones de correo electrónico (
//allow international characters), a su expresión regular le falta eluindicador nicode. Dicho esto, no recomiendo usar expresiones regulares para tratar de analizar / validar una dirección de correo electrónico porque a medida que su patrón de validación mejora su precisión, la legibilidad y el mantenimiento se desploman. yo recomiendofilter_var()https://stackoverflow.com/q/12026842/2943403$email_hostya contiene el dominio, así que no vuelva a realizar la cirugía conexplode()/array_pop().SELECT * FROM emailTable WHERE email=?está solicitando demasiados datos. Solo necesita losCOUNT()datos de la fila, no los. De esta manera, solo obtiene el recuento (que será cero o uno), por lo que la condición es muy simple y legible.Si desea SECAR las líneas "header & die", puede crear una pequeña función auxiliar que reciba el texto de ubicación, luego llame a
header(), luegodie()s.Recomendaría usar la sintaxis
<<<HTML ... HTML;(HEREDOC) para que pueda usar comillas dobles en su marcado y variables en línea. Alternativamente, podría usarsprintf(), pero eso es menos convincente con solo una variable.Dado querecomiendo, creando una serie de líneas$headerslas líneas están delimitados por\r\n,, a continuación,. El cuarto parámetro deimplode()ing con\r\nque sea más secomail()son los datos del encabezado que pueden recibir una matriz.No recomiendo el nativo
mail(). Siempre incorporo PHPMailer en todos mis proyectos para facilitar su uso y funcionalidad.Me preocupa la ambigüedad si
invalid.html. Simail()falla, el usuario no sabrá si hay algo que pueda arreglar o no.$userIP4 = ""; $dateTime = ""; $Temail = ""; $Acode = "";todo esto es una tontería inútil, simplemente omítelo todo.Examine su secuencia de comandos y busque variables de un solo uso. Cuando vea que una variable declarada solo se usa una vez, no es necesario que la declare.