Walidacja wiadomości e-mail w PHP

Oct 01 2020

Czy ten skrypt jest wystarczający do sprawdzenia poprawności danych wprowadzonych przez użytkownika?

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

Mając na uwadze zagrożenia bezpieczeństwa:

1. Wstrzyknięcia SQL !!! --- Rozwiązania: Gotowe instrukcje (PDO), używając tylko UTF-8 i zawierające "$ bpdo-> setAttribute (PDO :: ATTR_EMULATE_PREPARES, false);" w połączeniu z bazą danych

2. Ataki XSS !!! --- Rozwiązania: htmlspecialchars (), Polityka bezpieczeństwa treści (umieszczona w 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. Ataki poleceń systemu operacyjnego !!! --- Rozwiązania: usuwanie białych znaków (niepotrzebne w przypadku wiadomości e-mail) , sprawdzanie zgodności z białą listą dozwolonych wartości.

4. Ataki DOS !!! --- Rozwiązanie: nie zaimplementowano. Nie jestem pewien, czy konieczne są dodatkowe środki ostrożności, ponieważ w mojej witrynie nie ma możliwości logowania.

5. Wstrzyknięcie wiadomości e-mail PHP !!! --- Rozwiązanie: Wyrażenie regularne (to, które mam, jest zaprojektowane tak, aby zezwalać na znaki międzynarodowe).

Dodatkowo używam certyfikatu SSL, SiteLock Security- Essential, CloudFlare CDN i zaimplementowałem politykę DMARC w moim DNS (coś, co będę dostrajać w dającej się przewidzieć przyszłości).

Odpowiedzi

6 YourCommonSense Oct 01 2020 at 16:41

Przede wszystkim, niezależnie od jakichkolwiek rzeczywistych lub wyimaginowanych problemów z bezpieczeństwem, ten kod jest bólem w oku . Prawie niemożliwe jest zrozumienie istoty tego i udzielenie odpowiedzi na rzeczywiste pytanie, ponieważ kod nieustannie przesuwa się poza zasięg wzroku, a ogromna ilość kodu HTML przeszkadza w trakcie rzekomo e-mailowego kodu weryfikacyjnego. Przyczyniają się do tego również niepotrzebne weryfikacje.

Naprawdę powinieneś najpierw przepisać swój kod, aby był czytelny. W końcu jest to również kwestia bezpieczeństwa - w takiej dziczy łatwo jest przeoczyć prawdziwy problem. Daj swojemu kodowi trochę miłości :

  • przenieś połączenie z bazą danych do oddzielnego pliku, a następnie po prostu je dołącz. Przy okazji, oto jak to zrobić poprawnie . W tej chwili twoje połączenie PDO jest kwestią bezpieczeństwa, ponieważ ujawnienie komunikatu o błędzie systemu światu zewnętrznemu nie jest drugorzędne
  • stwórz funkcję do wysyłania e-maili, umieść tam cały ten $headers .= "From: MY NAME <[email protected]>\r\n"; biznes. następnie umieść samą funkcję w osobnym pliku, a następnie po prostu ją dołącz.
  • przenieś kod, aby wysłać faktyczny e-mail do funkcji i umieść tę funkcję na dole kodu. Więc nie odwróci uwagi czytelnika od głównego algorytmu.
  • pozbyć się bezużytecznego kodu. Sprawdzanie REMOTE_ADDR nie ma sensu, nie ma sytuacji, w której byłby nieprawidłowy. htmlspecialchars również jest tutaj bezużyteczny. i wyczyszczenie zmiennych, czyli $userIP4 = "";również
  • pozbyć się tej drabiny kodu. Biorąc pod uwagę warunki, mimo wszystko zatrzymaj wykonywanie - dlaczego po prostu nie przetestować pod kątem negatywnego wyniku i zatrzymać wykonywanie?

Więc zamiast

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

tylko napisz

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

Po przygotowaniu kodu do przeglądu możesz zadać nowe pytanie dotyczące obaw dotyczących bezpieczeństwa. Jak dotąd udało mi się dostrzec przynajmniej jednego.

5 Anonymous Oct 02 2020 at 04:19

Niepotrzebne ujawnianie informacji

Nie rób tego:

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

Pojawi się to w nagłówkach wiadomości i będzie widoczne dla odbiorcy. Nikogo nie interesuje, jakiej wersji PHP używasz. Jeśli używasz wersji PHP, która ma luki w zabezpieczeniach, zapewnia to hakerom wgląd w Twoje systemy w celu wykonania dostosowanego exploita. Po prostu nie kopiuj i wklejaj kodu bez zrozumienia konsekwencji ...

IPv4 i IPv6

Nie rozumiem sensu gethostbyname. Masz już adres IP, więc możesz go zapisać.

Czy na pewno Twój serwer nie jest osiągalny w Internecie przez IPv6? Możesz przetestować swoją witrynę przez IPv6, dodając wpis do swojej konfiguracji DNS, np. Ipv6.yoursite.com z jednym rekordem AAAA i bez rekordu A.

Randomizacja

Kod weryfikacyjny nie jest tak naprawdę przypadkowy :

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

time - Zwraca aktualny znacznik czasu Uniksa

Ktoś (haker), który zna twoją formułę, będzie w stanie złamać kod weryfikacyjny, ponieważ jest on zgodny z przewidywalnym wzorcem. Masz wiele lepszych opcji generowania rozsądnie losowych ciągów.

Różne

Prawdopodobnie zmieniłbym kolejność kodu, na przykład zapisałbym rekord w bazie danych przed wysłaniem wiadomości. Jeśli z jakiegoś powodu baza danych ulegnie awarii lub połączenie zostanie utracone, wysłałeś już wiadomość e-mail z kodem weryfikacyjnym, który nie jest nigdzie zapisywany i nie będzie działał - co jest mylące dla użytkownika. Jeśli z jakiegoś powodu rekord nie został zapisany, powiadom użytkownika, że ​​wystąpił błąd (i powiadom też siebie) i nie kontynuuj.

Prawdopodobnie wysyłanie poczty jest mniej prawdopodobne, ponieważ zwykle trafia do kolejki poczty obsługiwanej przez MTA na Twoim serwerze.

Możliwe, że wiadomość nie dotarła, została odrzucona lub zawierała spam, więc po pewnym czasie użytkownik powinien mieć możliwość zażądania nowego kodu.

4 mickmackusa Oct 01 2020 at 17:48
  • Jeśli chodzi o połączenie z bazą danych i obsługę błędów, przeczytaj tę odpowiedź . Nigdy nie możesz ujawniać generowanych przez system szczegółów błędów swoim użytkownikom końcowym - te informacje są dla Ciebie i nikogo innego. Polecam requirerozmowę telefoniczną, ale nie zanim zgłoszenie użytkownika kwalifikuje się do jego użycia.

  • Nie ma absolutnie żadnego powodu, dla którego należałoby modyfikować lub oczyszczać przychodzący adres e-mail. Możesz chcieć użyć białych znaków trim(), ale szczerze mówiąc, kto właściwie popełni błąd, dodając nieuczciwą spację? Nigdy ... przenigdy.

  • Aby zachować spójność, zawsze zapisuję gałęzie warunkowe ujemne / niepowodzenia / fałszywe przed gałęziami dodatnimi / pomyślnymi / prawdziwymi. W ten sposób Ty (lub inni programiści) będziecie wiedzieć, że im niższy jest postęp skryptu, tym skuteczniejszy był przepływ i można zainicjować / zadeklarować dodatkowe zasoby.

  • Nie generuj sygnatury czasowej dla wiersza db w php. Nie musisz nawet wspominać o tym w swoim sql. Ustaw emailTable„s datetimekolumny DEFAULTdo CURRENT_TIMESTAMP. https://stackoverflow.com/q/168736/2943403

  • Jeśli oczekiwaniem jest dopuszczenie znaków wielobajtowych w adresach e-mail ( //allow international characters), w wyrażeniu regularnym brakuje uflagi nicode. To powiedziawszy, nie polecam używania wyrażenia regularnego do prób analizowania / sprawdzania poprawności adresu e-mail, ponieważ gdy wzorzec walidacji poprawia jego dokładność, spada czytelność i łatwość konserwacji. polecamfilter_var() https://stackoverflow.com/q/12026842/2943403

  • $email_hostjuż zawiera domenę, więc nie wykonuj ponownie operacji za pomocą explode()/ array_pop().

  • SELECT * FROM emailTable WHERE email=?prosi o podanie zbyt dużej ilości danych. Potrzebujesz tylko COUNT()danych wiersza, a nie. W ten sposób pobierasz tylko liczbę (która będzie równa zero lub jeden), więc warunek jest bardzo prosty i czytelny.

  • Jeśli chcesz WYSUSZAĆ wiersze „nagłówek i umieść”, możesz utworzyć małą funkcję pomocniczą, która odbiera tekst lokalizacji, a następnie wywołuje polecenie header(), a następnie die()s.

  • Zalecałbym użycie składni <<<HTML ... HTML;( HEREDOC), aby można było używać podwójnych cudzysłowów w swoich znacznikach i zmiennych wbudowanych. Alternatywnie możesz użyć sprintf(), ale jest to mniej atrakcyjne w przypadku tylko jednej zmiennej.

  • Ponieważ $headerslinie są ograniczone \r\n, polecam, tworząc tablicę wierszy , a następnie implode()ing z \r\nbyć bardziej DRY . Czwartym parametrem mail()są dane nagłówkowe, które mogą przyjmować tablicę.

  • Nie polecam tubylcom mail(). Zawsze buduję PHPMailer we wszystkich moich projektach ze względu na łatwość użycia i funkcjonalność.

  • Martwi mnie dwuznaczność, jeśli invalid.html. Jeśli się mail()nie powiedzie, użytkownik nie będzie wiedział, czy jest coś, co może naprawić, czy nie.

  • $userIP4 = ""; $dateTime = ""; $Temail = ""; $Acode = ""; to wszystko jest bezużyteczne bzdury, po prostu pomiń to wszystko.

  • Przeszukaj swój skrypt i wyszukaj zmienne jednorazowego użytku. Kiedy widzisz zadeklarowaną zmienną używaną tylko raz, nie musisz jej deklarować.