Convalida e-mail in PHP

Oct 01 2020

Questo script è sufficiente per convalidare l'input di posta elettronica dell'utente?

<?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
?>

Minacce alla sicurezza in mente:

1. Iniezioni SQL !!! --- Soluzioni: Prepared Statements (PDO), utilizzando solo UTF-8 e includendo "$ bpdo-> setAttribute (PDO :: ATTR_EMULATE_PREPARES, false);" nella connessione al database

2. Attacchi XSS !!! --- Soluzioni: htmlspecialchars (), Content-Security Policy (inserita in 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. Attacchi ai comandi del sistema operativo !!! --- Soluzioni: striping degli spazi (non necessario con le email) , convalida rispetto a una whitelist di valori consentiti.

4. Attacchi DOS !!! --- Soluzione: nessuna implementata. Non sono sicuro se siano necessarie ulteriori precauzioni, poiché non ci sono possibilità di accesso sul mio sito web.

5. Iniezione di posta elettronica PHP !!! --- Soluzione: un'espressione regolare (quella che ho è principalmente progettata per consentire i caratteri internazionali).

Inoltre, utilizzo un certificato SSL, SiteLock Security- Essential, CloudFlare CDN e ho implementato una politica DMARC nel mio DNS (qualcosa che metterò a punto per il prossimo futuro).

Risposte

6 YourCommonSense Oct 01 2020 at 16:41

Prima di tutto, indipendentemente da qualsiasi problema di sicurezza reale o immaginario, questo codice è un dolore agli occhi . È quasi impossibile capirne l'essenza e rispondere alla domanda reale perché il codice si sposta costantemente fuori dalla vista e l'enorme quantità di HTML si intromette nel mezzo del presunto codice di verifica e-mail. A ciò si aggiungono anche inutili verifiche.

Dovresti davvero riscrivere il tuo codice prima, in modo da renderlo leggibile. Dopotutto, è anche una sorta di problema di sicurezza: in una tale natura selvaggia è facile trascurare un problema reale. Dai un po 'di amore al tuo codice :

  • spostare la connessione al database in un file separato e quindi includerla. A proposito, ecco come farlo correttamente . In questo momento la tua connessione PDO è un problema di sicurezza perché rivelare il messaggio di errore di sistema al mondo esterno non è di secondaria importanza
  • creare una funzione per inviare e-mail, mettere $headers .= "From: MY NAME <[email protected]>\r\n";lì tutto questo business. quindi metti la funzione stessa in un file separato e poi includila.
  • spostare il codice per inviare l'email effettiva in una funzione e inserire questa funzione nella parte inferiore del codice. Quindi non distrarrà un lettore dall'algoritmo principale.
  • sbarazzarsi del codice inutile. Controllare REMOTE_ADDR non ha senso, non c'è nessuna situazione in cui sarebbe non valido. Anche htmlspecialchars è inutile qui. e di compensazione le variabili, vale a dire $userIP4 = "";oltre
  • sbarazzarsi di quella scala a pioli di un codice. Date le tue condizioni, interrompi comunque l'esecuzione, perché non testare solo il risultato negativo e interrompere l'esecuzione?

Quindi invece di

if (condition) {
    if (condition2) {
        do something;
    } else {
        display error;
        die;
    } 
} else {
    display error;
    die;
}

Scrivi e basta

if (!condition) {
    display error;
    die;
}
if (!condition2) {
    display error;
    die;
}
do something;

Dopo aver reso il tuo codice adatto per la revisione, puoi fare una nuova domanda sui tuoi problemi di sicurezza. Di gran lunga sono riuscito a individuarne almeno uno.

5 Anonymous Oct 02 2020 at 04:19

Divulgazione di informazioni non necessarie

Non farlo:

$headers .= "X-Mailer: PHP". phpversion() ."\r\n" ;

Questo verrà visualizzato nelle intestazioni della posta e sarà visibile al destinatario. Non sono affari di nessuno quale versione di PHP stai utilizzando. Nel caso in cui tu stia eseguendo una versione di PHP che presenta vulnerabilità, questo fornisce agli hacker informazioni sui tuoi sistemi per un exploit personalizzato. Basta non copiare-incollare il codice senza capirne le implicazioni ...

IPv4 e IPv6

Non capisco il punto di gethostbyname. Hai già l'indirizzo IP, quindi puoi salvarlo.

Sei sicuro che il tuo server non sia raggiungibile su Internet tramite IPv6? Potresti voler testare il tuo sito su IPv6 aggiungendo una voce alla tua configurazione DNS, ad esempio ipv6.yoursite.com con un record AAAA e nessun record A.

Randomizzazione

Il codice di verifica non è realmente casuale :

$Acode = md5(time().$Temail);

time - Restituisce il timestamp Unix corrente

Qualcuno (un hacker) che conosce la tua formula sarà in grado di forzare il codice di verifica perché segue uno schema prevedibile. Hai molte opzioni migliori per generare stringhe ragionevolmente casuali.

Misc

Probabilmente dovrei riordinare un codice, ad esempio salvare il record nel database prima di inviare la posta. Se il database si arresta in modo anomalo per qualche motivo o la connessione viene persa, hai già inviato un'e-mail con un codice di verifica che non è registrato da nessuna parte e non funzionerà, il che crea confusione per l'utente. Se il record non è stato salvato per qualche motivo, informa l'utente che si è verificato un errore (e avvisa anche te stesso) e non procedere oltre.

L'invio della posta è probabilmente meno probabile che si blocchi, perché di solito va a una coda di posta per essere gestita dall'MTA sul tuo server.

È possibile che la posta non arrivi o sia stata scartata o bloccata da spam, quindi dovrebbe essere possibile per l'utente richiedere un nuovo codice dopo qualche tempo.

4 mickmackusa Oct 01 2020 at 17:48
  • Per quanto riguarda la connessione db e la gestione degli errori, leggi questa risposta . Non devi mai rivelare i dettagli degli errori generati dal sistema ai tuoi utenti finali: questi dettagli sono per te e per nessun altro. Raccomando una requirechiamata, ma non prima che l'invio dell'utente sia idoneo per il suo utilizzo.

  • Non c'è assolutamente alcun motivo per dover modificare o disinfettare l'indirizzo e-mail in arrivo. Potrebbe piacerti lo spazio bianco trim(), ma onestamente, chi commetterà effettivamente l'errore di aggiungere uno spazio canaglia? Non l'ho mai ... mai.

  • Per coerenza, scrivo sempre i miei rami condizionali negativi / falliti / falsi prima dei rami positivi / di successo / veri. In questo modo, tu (o altri sviluppatori) saprai che minore è l'avanzamento dello script, maggiore è stato il successo del flusso e ulteriori risorse possono essere inizializzate / dichiarate.

  • Non generare il timestamp per la riga db in php. Non è nemmeno necessario menzionarlo nel tuo sql. Impostare la emailTable's datetimecolonna DEFAULTa CURRENT_TIMESTAMP.https://stackoverflow.com/q/168736/2943403

  • Se l'aspettativa è di consentire caratteri multibyte negli indirizzi e-mail ( //allow international characters), nella tua regex manca il uflag nicode. Detto questo, non consiglio di usare regex per provare ad analizzare / convalidare un indirizzo e-mail perché quando il tuo modello di convalida migliora la sua precisione, la leggibilità e la manutenibilità precipitano. io raccomandofilter_var() https://stackoverflow.com/q/12026842/2943403

  • $email_hostcontiene già il dominio, quindi non eseguire più operazioni chirurgiche con explode()/ array_pop().

  • SELECT * FROM emailTable WHERE email=?sta chiedendo troppi dati. Hai solo bisogno dei dati COUNT(), non della riga. In questo modo, recuperi solo il conteggio (che sarà zero o uno), quindi la condizione è molto semplice e leggibile.

  • Se si desidera DRY le righe "header & die", è possibile creare una piccola funzione di supporto che riceva il testo della posizione, quindi chiami header(), quindi die()s.

  • Suggerirei di utilizzare la sintassi <<<HTML ... HTML;( HEREDOC) in modo da poter utilizzare virgolette doppie nel markup e nelle variabili inline. In alternativa, puoi usare sprintf(), ma è meno convincente con una sola variabile.

  • Poiché $headersle linee sono tutte delimitate da \r\n, vi consiglio, creando una matrice di linee , poi implode()re con \r\nessere più DRY . Il quarto parametro di mail()è i dati di intestazione che possono ricevere un array.

  • Non consiglio il nativo mail(). Ho sempre integrato PHPMailer in tutti i miei progetti per facilità d'uso e funzionalità.

  • Sono preoccupato per l'ambiguità se invalid.html. Se mail()fallisce, l'utente non saprà se c'è qualcosa che può aggiustare o meno.

  • $userIP4 = ""; $dateTime = ""; $Temail = ""; $Acode = ""; sono tutte sciocchezze inutili, omettetele tutte.

  • Passa in rassegna il tuo script e cerca variabili monouso. Quando vedi una variabile dichiarata usata solo una volta, non è necessario dichiararla.