Walidacja wiadomości e-mail w PHP
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
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.
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.
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„sdatetimekolumnyDEFAULTdoCURRENT_TIMESTAMP. https://stackoverflow.com/q/168736/2943403Jeśli oczekiwaniem jest dopuszczenie znaków wielobajtowych w adresach e-mail (
//allow international characters), w wyrażeniu regularnym brakujeuflagi 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 tylkoCOUNT()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ępniedie()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żpolecam, tworząc tablicę wierszy$headerslinie są ograniczone\r\n,, a następnie. Czwartym parametremimplode()ing z\r\nbyć bardziej DRYmail()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ć.