Tiếp Phần 8 (website đã đầy đủ chức năng bán hàng + quản trị). Website chạy được KHÔNG có nghĩa là website AN TOÀN. Phần này mình tổng hợp lại toàn bộ các lớp bảo mật đã áp dụng từ đầu loạt bài, và bổ sung thêm những lớp phòng thủ còn thiếu. Đây là phần các bạn nên đọc kỹ nhất trước khi đưa web lên internet thật. ============================== 1. RÀ SOÁT LẠI NHỮNG GÌ ĐÃ LÀM ĐÚNG TỪ ĐẦU ============================== - Chống SQL Injection: 100% câu truy vấn dùng PDO prepared statement ($pdo->prepare()->execute([...])), không nối chuỗi SQL trực tiếp ở bất kỳ đâu trong cả loạt bài. - Chống XSS: mọi dữ liệu (tên sản phẩm, mô tả, tên khách hàng, địa chỉ...) khi in ra HTML đều đi qua hàm e() (htmlspecialchars). - Chống CSRF: mọi form POST (đăng ký, đăng nhập, thêm giỏ hàng, đặt hàng, các form trong admin...) đều có csrf_field() và csrf_verify(). - Mật khẩu: dùng password_hash()/password_verify() (thuật toán bcrypt), không lưu plain text, không tự chế thuật toán mã hoá. - Chống session fixation: session_regenerate_id(true) sau mỗi lần đăng nhập/đăng ký thành công. - Chống IDOR (xem trộm dữ liệu người khác qua đổi ID trên URL): trang order_view.php, order_success.php luôn kiểm tra "WHERE id = ? AND user_id = ?". - Chống thao túng giá tiền: checkout.php luôn tính lại giá từ CSDL, không tin số tiền gửi từ session/form. - Chống race condition khi hết hàng: câu UPDATE stock kèm điều kiện "AND stock >= quantity" chạy trong transaction. - Upload ảnh: whitelist đuôi file + kiểm tra MIME thật bằng finfo + đổi tên file ngẫu nhiên + giới hạn dung lượng. Nếu bạn đã dán đủ code từ Phần 1-8, các lớp bảo vệ trên đã có sẵn trong dự án rồi. Giờ bổ sung thêm. ============================== 2. NÂNG CẤP CẤU HÌNH SESSION CHO AN TOÀN HƠN ============================== Sửa lại đoạn mở đầu của includes/functions.php (đã tạo ở Phần 3) thành như sau - thêm cấu hình cookie session trước khi session_start(): <?php if (session_status() === PHP_SESSION_NONE) { session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'httponly' => true, // JavaScript khong doc duoc cookie session -> giam rui ro XSS danh cap session 'samesite' => 'Lax', // trinh duyet khong gui cookie nay kem theo request tu trang web khac -> giam rui ro CSRF ]); session_start(); } require_once __DIR__ . '/../config/database.php'; // ... (giu nguyen phan con lai nhu Phan 3) Nếu website của bạn chạy HTTPS (khuyến nghị bắt buộc khi lên hosting thật), thêm 'secure' => true vào mảng trên để cookie chỉ được gửi qua kết nối mã hoá. ============================== 3. CHẶN BRUTE-FORCE ĐĂNG NHẬP (giới hạn số lần đăng nhập sai) ============================== Thêm đoạn kiểm tra sau vào ĐẦU login.php (cả login.php của khách và admin/login.php), ngay sau dòng require_once functions.php: <?php // Gioi han 5 lan dang nhap sai trong 5 phut cho moi email, chan brute-force function too_many_attempts(string $email): bool { $key = 'login_attempts_' . md5($email); $data = $_SESSION[$key] ?? ['count' => 0, 'time' => time()]; if (time() - $data['time'] > 300) { $data = ['count' => 0, 'time' => time()]; } $_SESSION[$key] = $data; return $data['count'] >= 5; } function record_failed_attempt(string $email): void { $key = 'login_attempts_' . md5($email); $_SESSION[$key]['count'] = ($_SESSION[$key]['count'] ?? 0) + 1; $_SESSION[$key]['time'] = $_SESSION[$key]['time'] ?? time(); } function reset_attempts(string $email): void { unset($_SESSION['login_attempts_' . md5($email)]); } Sau đó trong khối xử lý POST của login.php, bọc logic kiểm tra mật khẩu lại như sau (ví dụ áp dụng cho login.php của khách hàng, admin/login.php làm tương tự): if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; if (too_many_attempts($email)) { $error = 'Ban da nhap sai qua nhieu lan, vui long thu lai sau 5 phut.'; } else { $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? LIMIT 1'); $stmt->execute([$email]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password_hash'])) { reset_attempts($email); $_SESSION['user_id'] = (int) $user['id']; $_SESSION['user_name'] = $user['full_name']; $_SESSION['user_role'] = $user['role']; session_regenerate_id(true); redirect($user['role'] === 'admin' ? 'admin/index.php' : 'account.php'); } record_failed_attempt($email); $error = 'Email hoac mat khau khong dung.'; } } (Lưu ý: đây là cách giới hạn đơn giản dựa trên session, đủ dùng cho website nhỏ. Nếu website lớn hơn, cách chuẩn hơn là lưu số lần đăng nhập sai vào 1 bảng CSDL riêng theo IP + email, giống hệ thống diễn đàn này đang áp dụng.) ============================== 4. CHẶN TRUY CẬP TRỰC TIẾP CÁC THƯ MỤC NHẠY CẢM ============================== Thư mục config/ (chứa thông tin kết nối CSDL) và database/ (chứa file .sql) TUYỆT ĐỐI không được để ai truy cập trực tiếp qua trình duyệt (VD: gõ shopvn/config/database.php thấy lộ mật khẩu CSDL, hoặc shopvn/database/shopvn.sql tải được nguyên cấu trúc + dữ liệu). Nếu dùng Apache (XAMPP/Laragon/hosting cPanel): tạo file .htaccess đặt NGAY BÊN TRONG từng thư mục config/ và database/, nội dung: # File: config/.htaccess va database/.htaccess (noi dung giong nhau) <IfModule mod_authz_core.c> Require all denied </IfModule> <IfModule !mod_authz_core.c> Deny from all </IfModule> Nếu dùng IIS (Windows Server): tạo file web.config đặt trong từng thư mục config/ và database/: <?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <security> <authorization> <clear /> </authorization> </security> <httpErrors> <remove statusCode="403" /> <error statusCode="403" path="/index.php" responseMode="Redirect" /> </httpErrors> </system.webServer> </configuration> Cách đơn giản và chắc chắn hơn nữa (khuyến nghị khi lên hosting thật): nếu hosting cho phép cấu hình document root, hãy đặt document root của domain trỏ thẳng vào 1 thư mục con (VD public/) chỉ chứa các file .php cần truy cập từ trình duyệt (index.php, product.php, assets/...), còn config/ và database/ đặt ở thư mục cha - lúc đó các thư mục này nằm NGOÀI vùng mà web server có thể phục vụ, an toàn tuyệt đối dù quên cấu hình .htaccess. ============================== 5. BẢO VỆ THƯ MỤC UPLOADS KHỎI THỰC THI MÃ ĐỘC ============================== Đây là lỗ hổng nghiêm trọng hay bị bỏ sót: dù Phần 8 đã kiểm tra kỹ file upload, vẫn nên chặn thêm 1 lớp ở cấp thư mục - CẤM thực thi bất kỳ file PHP nào trong assets/uploads/, phòng trường hợp có lỗ hổng nào đó lọt qua khiến kẻ xấu upload được file .php. Tạo file assets/uploads/.htaccess (Apache): <FilesMatch "\.(php|phtml|php3|php4|php5|php7|phar|pht)$"> <IfModule mod_authz_core.c> Require all denied </IfModule> <IfModule !mod_authz_core.c> Deny from all </IfModule> </FilesMatch> Nếu dùng IIS, tạo assets/uploads/web.config: <?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <handlers> <remove name="PHP_via_FastCGI" /> </handlers> </system.webServer> </configuration> (Nội dung web.config phần handlers có thể khác nhau tuỳ cấu hình FastCGI cụ thể của hosting - nếu không chắc, cách an toàn nhất và đơn giản nhất là hỏi nhà cung cấp hosting cách "vô hiệu hoá thực thi PHP trong 1 thư mục con" trên IIS.) ============================== 6. TẮT HIỂN THỊ LỖI CHI TIẾT KHI LÊN HOSTING THẬT ============================== Khi code đang chạy trên máy cá nhân (localhost) để học/test, hiển thị lỗi chi tiết (display_errors on) giúp debug nhanh. Nhưng khi đưa lên hosting thật, PHẢI tắt đi - vì lỗi chi tiết có thể lộ đường dẫn thư mục, tên bảng, thậm chí một phần câu SQL cho bất kỳ ai ghé thăm site đúng lúc site bị lỗi. Thêm vào đầu file config/database.php (trước đoạn kết nối PDO): <?php // Doi thanh false khi dua len hosting that $is_development = true; if ($is_development) { ini_set('display_errors', '1'); error_reporting(E_ALL); } else { ini_set('display_errors', '0'); error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE); } ini_set('log_errors', '1'); Nhớ đổi $is_development thành false ngay khi deploy thật (Phần 10 sẽ nhắc lại việc này trong checklist go-live). ============================== 7. TÓM TẮT CHECKLIST BẢO MẬT TRƯỚC KHI LÊN HOSTING ============================== - [ ] Toàn bộ SQL dùng prepared statement, không nối chuỗi. - [ ] Toàn bộ output HTML escape qua e(). - [ ] Toàn bộ form POST có CSRF token. - [ ] Mật khẩu dùng password_hash()/password_verify(). - [ ] session_regenerate_id() sau đăng nhập/đăng ký. - [ ] Cookie session bật httponly + samesite (và secure nếu có HTTPS). - [ ] Giới hạn số lần đăng nhập sai. - [ ] Chặn truy cập trực tiếp config/ và database/. - [ ] Chặn thực thi PHP trong assets/uploads/. - [ ] Upload file kiểm tra đủ đuôi + MIME thật + đổi tên ngẫu nhiên. - [ ] Tắt display_errors, bật log_errors ở môi trường production. - [ ] Đổi mật khẩu tài khoản admin mặc định trước khi công khai website. Xong phần bảo mật, website ShopVN của chúng ta giờ đã tương đối vững chắc để đưa ra thực tế. Phần 10 (phần cuối cùng) mình sẽ hướng dẫn tối ưu hiệu năng (phân trang, index CSDL, nén ảnh) và các bước triển khai (deploy) website lên hosting thật từ A-Z. Hẹn gặp lại phần cuối! --- Hết Phần 9 --- ---------------------------------------- MỤC LỤC LOẠT BÀI: 1. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 1: Tổng Quan Dự Án, Yêu Cầu Môi Trường & Cấu Trúc Thư Mục - https://diendan.anyvlogs.info/chu-de/457-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-1-tong-quan-du-an-yeu-cau-moi-truong-cau-truc-thu-muc 2. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 2: Thiết Kế Cơ Sở Dữ Liệu (Database) Đầy Đủ Cho Website Bán Hàng - https://diendan.anyvlogs.info/chu-de/458-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-2-thiet-ke-co-so-du-lieu-database-day-du-cho-website-ban-hang 3. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 3: Dựng Khung Ứng Dụng - Kết Nối PDO, Hàm Dùng Chung, Giao Diện Chung - https://diendan.anyvlogs.info/chu-de/459-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-3-dung-khung-ung-dung-ket-noi-pdo-ham-dung-chung-giao-dien-chung 4. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 4: Trang Chủ, Danh Mục Sản Phẩm & Chi Tiết Sản Phẩm - https://diendan.anyvlogs.info/chu-de/460-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-4-trang-chu-danh-muc-san-pham-chi-tiet-san-pham 5. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 5: Xây Dựng Giỏ Hàng (Cart) Bằng Session - https://diendan.anyvlogs.info/chu-de/461-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-5-xay-dung-gio-hang-cart-bang-session 6. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 6: Đăng Ký, Đăng Nhập, Đăng Xuất Thành Viên - https://diendan.anyvlogs.info/chu-de/462-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-6-dang-ky-dang-nhap-dang-xuat-thanh-vien 7. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 7: Đặt Hàng, Thanh Toán COD & Lịch Sử Đơn Hàng - https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang 8. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 8: Trang Quản Trị (Admin) - Quản Lý Danh Mục, Sản Phẩm, Đơn Hàng - https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang >> 9. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 9: Bảo Mật Website (CSRF, XSS, SQL Injection, Upload An Toàn) - https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan 10. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 10 (Kết Thúc): Tối Ưu Hiệu Năng & Triển Khai Lên Hosting Thật - https://diendan.anyvlogs.info/chu-de/466-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-10-ket-thuc-toi-uu-hieu-nang-trien-khai-len-hosting-that Phần trước: https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang Phần sau: https://diendan.anyvlogs.info/chu-de/466-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-10-ket-thuc-toi-uu-hieu-nang-trien-khai-len-hosting-that