Xác thực Email bằng PHP

Oct 01 2020

Tập lệnh này có đủ để xác thực đầu vào email của người dùng không?

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

Các mối đe dọa bảo mật trong tâm trí:

1. Tiêm SQL !!! --- Giải pháp: Câu lệnh chuẩn bị (PDO), chỉ sử dụng UTF-8 và bao gồm "$ bpdo-> setAttribute (PDO :: ATTR_EMULATE_PREPARES, false);" trong kết nối cơ sở dữ liệu

2. Các cuộc tấn công XSS !!! --- Giải pháp: htmlspecialchars (), Chính sách bảo mật-nội dung (được đặt trong 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. Lệnh tấn công hệ điều hành !!! --- Giải pháp: Loại bỏ khoảng trắng (không cần thiết với email) , xác thực dựa trên danh sách trắng các giá trị được phép.

4. Các cuộc tấn công DOS !!! --- Giải pháp: Không triển khai. Tôi không chắc liệu có cần thêm biện pháp phòng ngừa nào không vì không có khả năng đăng nhập trên trang web của tôi.

5. Chèn Email PHP !!! --- Giải pháp: Biểu thức chính quy (biểu thức tôi có hầu hết được thiết kế để cho phép các ký tự quốc tế).

Ngoài ra, tôi sử dụng Chứng chỉ SSL, SiteLock Security- Essential, CloudFlare CDN và đã triển khai Chính sách DMARC trong DNS của mình (điều gì đó tôi sẽ tinh chỉnh trong tương lai gần).

Trả lời

6 YourCommonSense Oct 01 2020 at 16:41

Đầu tiên và quan trọng nhất, bất kể bất kỳ vấn đề bảo mật thực tế hay tưởng tượng nào, mã này đều gây nhức mắt . Gần như không thể hiểu được ý chính của nó và trả lời câu hỏi thực tế vì mã liên tục chuyển ra khỏi tầm nhìn và lượng lớn HTML cản trở mã xác minh email được cho là. Những xác minh vô ích cũng thêm vào đó.

Bạn thực sự nên viết lại mã của mình trước để làm cho nó có thể đọc được. Rốt cuộc, đó cũng là một vấn đề bảo mật - trong một vùng hoang dã như vậy, thật dễ dàng để bỏ qua một vấn đề thực sự. Cung cấp cho mã của bạn một số tình yêu :

  • di chuyển kết nối cơ sở dữ liệu vào một tệp riêng biệt và sau đó chỉ cần bao gồm nó. Nhân tiện, đây là cách làm đúng cách . Hiện tại, kết nối PDO của bạn là một vấn đề bảo mật vì việc tiết lộ thông báo lỗi hệ thống cho thế giới bên ngoài không phải là một vấn đề nhỏ
  • tạo một chức năng để gửi email, đặt tất cả $headers .= "From: MY NAME <[email protected]>\r\n"; doanh nghiệp này vào đó. sau đó đặt chính hàm vào một tệp riêng biệt và sau đó chỉ cần đưa vào.
  • chuyển mã để gửi email thực vào một chức năng và đặt chức năng này ở cuối mã. Vì vậy, nó sẽ không làm người đọc phân tâm khỏi thuật toán chính.
  • thoát khỏi mã vô dụng. Việc kiểm tra REMOTE_ADDR không có ý nghĩa gì, không có trường hợp nào nó không hợp lệ. htmlspecialchars cũng vô dụng ở đây. và xóa các biến của bạn, tức $userIP4 = "";là
  • loại bỏ mã bước đó. Với các điều kiện của bạn, hãy dừng việc thực thi - tại sao không chỉ kiểm tra kết quả âm tính và dừng thực hiện?

Vì vậy, thay vì

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

chỉ viết

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

Sau khi làm cho mã của bạn phù hợp để xem xét, bạn có thể đặt một câu hỏi mới liên quan đến các mối lo ngại về bảo mật của mình. Cho đến nay tôi đã có thể phát hiện ra ít nhất một.

5 Anonymous Oct 02 2020 at 04:19

Tiết lộ thông tin không cần thiết

Đừng làm điều này:

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

Điều này sẽ hiển thị trong tiêu đề thư và người nhận có thể nhìn thấy. Bạn đang chạy phiên bản PHP nào là việc của không ai. Trong trường hợp bạn đang chạy một phiên bản PHP có lỗ hổng, điều này cung cấp cho tin tặc cái nhìn sâu sắc về hệ thống của bạn để có cách khai thác phù hợp. Chỉ cần không sao chép-dán mã mà không hiểu ý nghĩa ...

IPv4 & IPv6

Tôi không hiểu điểm của gethostbyname. Bạn đã có địa chỉ IP, vì vậy bạn có thể lưu trữ nó.

Bạn có chắc máy chủ của mình không thể truy cập được trên Internet qua IPv6 không? Bạn có thể muốn kiểm tra trang web của mình qua IPv6 bằng cách thêm một mục vào cấu hình DNS của bạn, ví dụ: ipv6.yoursite.com với một bản ghi AAAA và không có bản ghi A.

Ngẫu nhiên hóa

Mã xác minh không thực sự ngẫu nhiên :

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

time - Trả về dấu thời gian Unix hiện tại

Một người nào đó (tin tặc) biết công thức của bạn sẽ có thể thực thi mã xác minh vì nó tuân theo một mẫu có thể đoán trước được. Bạn có nhiều tùy chọn tốt hơn để tạo chuỗi ngẫu nhiên hợp lý.

Misc

Tôi có thể sẽ sắp xếp lại một số mã, ví dụ như lưu bản ghi vào cơ sở dữ liệu trước khi gửi thư. Nếu cơ sở dữ liệu gặp sự cố vì lý do nào đó hoặc kết nối bị mất, bạn đã gửi một E-mail có mã xác minh không được ghi ở bất kỳ đâu và sẽ không hoạt động - điều này gây nhầm lẫn cho người dùng. Nếu bản ghi không được lưu vì lý do nào đó, hãy thông báo cho người dùng rằng đã xảy ra lỗi (và thông báo cho chính bạn) và đừng tiếp tục thêm.

Việc gửi thư có lẽ ít gặp sự cố hơn, vì nó thường đi đến hàng đợi thư để được xử lý bởi MTA trên máy chủ của bạn.

Có thể thư không đến hoặc đã bị loại bỏ hoặc bị mắc kẹt trong thư rác, vì vậy người dùng có thể yêu cầu mã mới sau một thời gian.

4 mickmackusa Oct 01 2020 at 17:48
  • Về kết nối db và xử lý lỗi, vui lòng đọc câu trả lời này . Bạn không bao giờ được tiết lộ chi tiết lỗi do hệ thống tạo ra cho người dùng cuối của mình - những chi tiết này dành cho bạn chứ không phải ai khác. Tôi khuyên bạn nên requiregọi, nhưng không phải trước khi người dùng gửi đủ điều kiện để sử dụng.

  • Hoàn toàn không có lý do gì cần phải thay đổi hoặc khử trùng địa chỉ email đến. Bạn có thể thích khoảng trắng trim(), nhưng thành thật mà nói, ai thực sự sẽ mắc sai lầm khi thêm khoảng trắng ? Tôi chưa bao giờ ... bao giờ.

  • Để nhất quán, tôi luôn viết các nhánh điều kiện phủ định / thất bại / sai của mình trước các nhánh tích cực / thành công / đúng. Bằng cách này, bạn (hoặc các nhà phát triển khác) sẽ biết rằng tiến trình của tập lệnh càng thấp, thì luồng càng thành công và có thể khởi tạo / khai báo thêm các tài nguyên bổ sung.

  • Không tạo dấu thời gian cho hàng db trong php. Bạn thậm chí không cần phải đề cập đến nó trong sql của mình. Đặt của bạn emailTable's datetimecột để DEFAULTđến CURRENT_TIMESTAMP. https://stackoverflow.com/q/168736/2943403

  • Nếu kỳ vọng là cho phép các ký tự nhiềubyte trong địa chỉ email ( //allow international characters), thì regex của bạn thiếu ucờ nicode. Điều đó nói rằng, tôi không khuyên bạn nên sử dụng regex để cố gắng phân tích cú pháp / xác thực địa chỉ email vì khi mẫu xác thực của bạn cải thiện độ chính xác của nó, khả năng đọc và khả năng bảo trì sẽ giảm mạnh. Tôi đề nghịfilter_var() https://stackoverflow.com/q/12026842/2943403

  • $email_hostđã chứa miền, vì vậy không thực hiện phẫu thuật lại với explode()/ array_pop().

  • SELECT * FROM emailTable WHERE email=?đang yêu cầu quá nhiều dữ liệu. Bạn chỉ cần COUNT(), không cần dữ liệu của hàng. Bằng cách này, bạn chỉ tìm nạp số lượng (sẽ bằng 0 hoặc một), do đó, điều kiện rất đơn giản và dễ đọc.

  • Nếu bạn muốn KHÔ ra "tiêu đề & chết" dòng, bạn có thể tạo một hàm helper nhỏ mà nhận được văn bản Vị trí, sau đó gọi header(), sau đó die()s.

  • Tôi khuyên bạn nên sử dụng cú pháp <<<HTML ... HTML;( HEREDOC) để bạn có thể sử dụng dấu ngoặc kép trong các biến đánh dấu và nội dòng của mình. Ngoài ra, bạn có thể sử dụng sprintf(), nhưng điều đó kém hấp dẫn hơn chỉ với một biến.

  • Kể từ khi $headersdòng đều giới hạn bởi \r\n, tôi khuyên bạn nên tạo một mảng các dòng , sau đó implode()ing với \r\nđược khô hơn . Tham số thứ tư mail()là dữ liệu tiêu đề có thể nhận một mảng.

  • Tôi không giới thiệu người bản xứ mail(). Tôi luôn xây dựng PHPMailer vào tất cả các dự án của mình để dễ sử dụng và có chức năng.

  • Tôi lo ngại về sự mơ hồ nếu invalid.html. Nếu mail()thất bại, người dùng sẽ không biết liệu họ có thể sửa chữa được điều gì hay không.

  • $userIP4 = ""; $dateTime = ""; $Temail = ""; $Acode = ""; Đây là tất cả những điều vô nghĩa vô ích, chỉ cần bỏ qua tất cả.

  • Lược qua tập lệnh của bạn và tìm kiếm các biến sử dụng một lần. Khi bạn thấy một biến đã khai báo chỉ được sử dụng một lần, thì bạn không cần phải khai báo nó.