Chuyển đến nội dung chính (Skip to content)
Mục lục bài viết
PHP

PHP EP 17: Xử lý form HTTP và mã hóa đầu ra trong PHP

PHP EP 17: Xử lý form HTTP và mã hóa đầu ra trong PHP

Phân tích vòng đời HTTP Request/Response trong PHP qua $_GET, $_POST, $_SERVER, thực hành kiểm tra dữ liệu bằng filter_var và mã hóa đầu ra chống tấn công XSS.

Dữ liệu HTTP cần được kiểm tra trước khi sử dụng

Khi chuyển từ kịch bản CLI sang ứng dụng Web HTTP, toàn bộ dữ liệu đi vào chương trình qua các biến siêu toàn cục ($_GET, $_POST, $_COOKIE, $_SERVER) đều nằm dưới quyền kiểm soát của máy khách. Kẻ tấn công có thể truyền một chuỗi chữ vào trường số lượng, truyền mảng ?id[]=1 vào tham số kỳ vọng chuỗi đơn, giả mạo header HTTP_X_FORWARDED_FOR, hoặc chèn đoạn mã <script>alert("xss")</script> vào ô ghi chú biểu mẫu. Nếu ứng dụng nối thẳng chuỗi đó vào trang HTML trả về, trình duyệt của người dùng khác sẽ tự động thực thi mã độc (Cross-Site Scripting - XSS).

Bài học này giúp bạn nắm vững vòng đời xử lý yêu cầu HTTP trong PHP 8.4, phân biệt các biến siêu toàn cục, xây dựng lớp xác thực đầu vào (Input Validation) bằng filter_var(), và áp dụng mã hóa đầu ra (Output Escaping) bằng htmlspecialchars() để chặn đứng lỗ hổng XSS. Bài viết nối tiếp PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP và chuẩn bị cho PHP EP 18: Cookie, Session, CSRF và băm mật khẩu trong PHP. Tài liệu tham chiếu gồm PHP Manual: Superglobals, Data Filtering và htmlspecialchars.

Hồ sơ kiểm chứng & Môi trường Lab (Verification Profile):

  • Ngày kiểm chứng: 2026-10-26.
  • Môi trường thực thi: Container cô lập php-course-qa:8.4 (--network none, --read-only, --tmpfs /tmp:rw,exec,nosuid,size=64m, user phplab UID 1001).
  • Phiên bản runtime: PHP 8.4.27 (cli) trên Alpine Linux 3.21 (aarch64), Zend Engine v4.4.27, phần mở rộng filter biên dịch sẵn.
  • Giới hạn kiểm chứng: Kiểm chứng filter_var() và htmlspecialchars() với dữ liệu biểu mẫu mô phỏng trong container cô lập; không tuyên bố kiểm chứng chính sách Content-Security-Policy (CSP) hay WAF.

Phân biệt biến HTTP, bộ lọc và mã hóa đầu ra

Bảng dưới đây tổng hợp hợp đồng kỹ thuật của các công cụ xử lý HTTP trong PHP 8.4:

Thành phần Cú pháp / Cờ hiệu chuẩn Hành vi thực tế trong PHP 8.4 Điều kiện / Không nên giả định
$_GET và $_POST Đọc kèm toán tử ?? và kiểm tra is_string() Chứa dữ liệu từ query string URL hoặc thân biểu mẫu; mọi giá trị lá đều là string. Nếu client gửi ?page[]=1, giá trị $_GET['page'] sẽ là array chứ không phải string (gọi trim() sẽ gây TypeError!).
php://input (Raw Body) file_get_contents('php://input') Đọc thân request nguyên bản khi client gửi dữ liệu dạng application/json (vì $_POST rỗng với JSON). Luôn giải mã bằng json_decode(..., true, 512, JSON_THROW_ON_ERROR).
$_SERVER $_SERVER['REQUEST_METHOD'], $_SERVER['REMOTE_ADDR'] Chứa thông tin môi trường SAPI và các header HTTP (khóa có tiền tố HTTP_*). Mọi khóa bắt đầu bằng HTTP_* đều do client gửi lên và có thể bị giả mạo.
Xác thực đầu vào (filter_var) filter_var($v, FILTER_VALIDATE_INT, ['options' => ...]) Trả về giá trị đã chuyển đổi đúng kiểu (int, string...) nếu hợp lệ, hoặc trả về false nếu thất bại. Phải dùng so sánh chặt === false để kiểm tra lỗi (vì số nguyên hợp lệ 0 nếu dùng !$val sẽ bị coi nhầm là lỗi).
Mã hóa đầu ra HTML htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') Biến các ký tự & < > " ' thành thực thể HTML an toàn (`&lt;`, `&quot;`...) và thay byte UTF-8 lỗi. Chỉ bảo vệ khi đặt vào nội dung thẻ hoặc thuộc tính có dấu nháy; không tự bảo vệ nếu chèn vào URL javascript:.

Validation và escaping giải quyết hai vấn đề khác nhau

Một sai lầm kiến trúc phổ biến là gọi htmlspecialchars($_POST['title']) ngay trước khi lưu vào cơ sở dữ liệu. Cách làm này làm biến dạng dữ liệu gốc trong DB (chữ A & B bị lưu thành A &amp; B, gây sai lệch độ dài chuỗi khi tìm kiếm hoặc khi xuất ra JSON API/Mobile App).

Chuẩn bảo mật ứng dụng web hiện đại tách bạch hai lớp phòng thủ tại hai biên:

  1. Tại biên tiếp nhận Request (Input Validation): Kiểm tra kiểu chuỗi đơn (is_string()), cắt khoảng trắng bằng mb_trim(), và xác thực định dạng/miền giá trị bằng filter_var() (FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_INT kèm min_range/max_range). Nếu dữ liệu sai quy tắc, từ chối ngay với mã HTTP 422. Nếu hợp lệ, giữ nguyên văn bản gốc sạch cho tầng nghiệp vụ.
  2. Tại biên hiển thị HTML (Output Escaping): Ngay tại thời điểm ghép biến vào khuôn mẫu HTML trả về cho trình duyệt, luôn gọi htmlspecialchars($val, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). Cờ ENT_QUOTES mã hóa cả dấu nháy kép " lẫn dấu nháy đơn ', ngăn kẻ tấn công thoát khỏi thuộc tính HTML value='...'.
Luồng tiếp nhận yêu cầu HTTP, xác thực dữ liệu đầu vào và mã hóa trước khi hiển thị.
Hình 1. Luồng tiếp nhận yêu cầu HTTP, xác thực dữ liệu đầu vào và mã hóa trước khi hiển thị. Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

Kiểm tra form và mã hóa đầu ra HTML

Chúng ta sẽ viết hàm xử lý dữ liệu biểu mẫu tiếp nhận email, mức độ ưu tiên (1..5) và ghi chú có chứa thẻ <script> độc hại, xác thực bằng filter_var() và mã hóa đầu ra bằng htmlspecialchars().

Bước 1: Tạo vùng làm việc tạm an toàn

Mục đích: Khởi tạo thư mục thực hành riêng trong /tmp.

WORKDIR="$(mktemp -d /tmp/php-ep17-XXXXXX)"
cd "$WORKDIR"
pwd

Bước 2: Tạo kịch bản http_filter_xss.php xác thực đầu vào và thoát mã XSS

Mục đích: Kiểm tra email bằng FILTER_VALIDATE_EMAIL, kiểm tra số nguyên 1..5 bằng FILTER_VALIDATE_INT và vô hiệu hóa thẻ <script> bằng htmlspecialchars().

cat <<'EOF' > "$WORKDIR/http_filter_xss.php"
<?php
declare(strict_types=1);

function sanitizeForm(array $post): array
{
    $email = filter_var(trim((string) ($post['email'] ?? '')), FILTER_VALIDATE_EMAIL);
    $priority = filter_var($post['priority'] ?? null, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1, 'max_range' => 5],
    ]);
    if ($email === false || $priority === false) {
        throw new InvalidArgumentException('Invalid form input');
    }
    $safeNote = htmlspecialchars((string) ($post['note'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    return ['email' => $email, 'priority' => $priority, 'safeNote' => $safeNote];
}

$res = sanitizeForm([
    'email'    => ' support@thanhnh.id.vn ',
    'priority' => '3',
    'note'     => '<script>alert("xss")</script>',
]);

echo "Email={$res['email']} | Prio={$res['priority']} | Escaped={$res['safeNote']}\n";
EOF
php "$WORKDIR/http_filter_xss.php"

Kết quả quan sát được:

  • Đầu ra hiển thị: Email=support@thanhnh.id.vn | Prio=3 | Escaped=&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;.
  • Chuỗi '3' được chuyển thành số nguyên int(3), và đoạn mã <script> bị biến thành văn bản hiển thị vô hại.

Cảnh báo an toàn: Hằng số FILTER_SANITIZE_STRING cũ đã bị loại bỏ từ PHP 8.1+. Hãy luôn dùng FILTER_VALIDATE_* ở đầu vào và htmlspecialchars() ở đầu ra HTML.

Chặn TypeError khi query string chứa mảng

Khi ứng dụng bật strict_types=1, nếu kẻ tấn công đổi URL từ ?q=php thành ?q[]=php, $_GET['q'] sẽ trở thành array và làm hàm mb_trim(string $s) ném TypeError.

Bước 3: Tạo kịch bản scalar_param_guard.php đọc tham số chuỗi đơn an toàn

Mục đích: Viết hàm readStringParam() chỉ chấp nhận chuỗi đơn và từ chối ngay nếu client gửi mảng.

cat <<'EOF' > "$WORKDIR/scalar_param_guard.php"
<?php
declare(strict_types=1);

function readStringParam(array $source, string $key, string $default = ''): string
{
    $val = $source[$key] ?? $default;
    if (!is_string($val)) {
        throw new InvalidArgumentException("Parameter '{$key}' must be a scalar string.");
    }
    return mb_trim($val);
}

echo "Normal=" . readStringParam(['q' => '  php 8.4 '], 'q') . "\n";
EOF
php "$WORKDIR/scalar_param_guard.php"

Kết quả quan sát được:

  • Màn hình in ra: Normal=php 8.4. Nếu client gửi mảng, hàm chủ động ném InvalidArgumentException để trả về HTTP 400 thay vì lỗi hệ thống 500.

Cảnh báo an toàn: Khi đặt URL của người dùng vào <a href="...">, ngoài htmlspecialchars(), bạn phải kiểm tra URL bắt đầu bằng https:// hoặc http:// để chặn giao thức javascript:alert(1).

Từ chối dữ liệu ngoài miền hợp lệ và dọn dẹp

Hãy kiểm chứng cách hàm sanitizeForm() chặn đứng dữ liệu sai định dạng email và mức ưu tiên vượt ngưỡng 1..5.

Bước 4: Truyền dữ liệu biểu mẫu sai quy tắc vào sanitizeForm() và kiểm tra mã thoát

Mục đích: Xác nhận filter_var() trả về false và kích hoạt InvalidArgumentException.

set +e
php -r '
require "'"$WORKDIR"'/http_filter_xss.php";
sanitizeForm(["email" => "bad-email", "priority" => "9"]);
'
RC=$?
set -e
echo "RC=$RC"

Kết quả quan sát được:

  • Tiến trình dừng với RC=255 do ngoại lệ InvalidArgumentException: Invalid form input được ném ra ngay tại bước kiểm tra đầu vào.
Phân biệt nhiệm vụ lọc dữ liệu đầu vào (Validation) và thoát ký tự đầu ra (Escaping).
Hình 2. Phân biệt nhiệm vụ lọc dữ liệu đầu vào (Validation) và thoát ký tự đầu ra (Escaping). Sơ đồ minh họa khái niệm; không phải ảnh chụp hay bản ghi kiểm thử. Mở hình để xem chi tiết.

Bước 5: Dọn dẹp vùng làm việc tạm

Mục đích: Xóa thư mục thực hành trong /tmp.

rm -rf "$WORKDIR"

Kiểm soát tải tệp và header bảo mật

Khi triển khai ứng dụng xử lý biểu mẫu trên Production, cần bổ sung hai lớp bảo vệ:

  1. Xử lý tải tệp ($_FILES) an toàn: Không tin tưởng $_FILES['f']['type']. Hãy kiểm tra UPLOAD_ERR_OK, dùng finfo_file() đọc chữ ký MIME thực tế, đổi tên tệp thành chuỗi ngẫu nhiên (bin2hex(random_bytes(16))) và lưu ngoài thư mục chạy mã PHP.
  2. Thiết lập HTTP Security Headers: Trả kèm các header X-Content-Type-Options: nosniff, X-Frame-Options: DENY và Content-Security-Policy.

Tự kiểm tra và giới hạn

Hãy tự kiểm tra lại kiến thức qua 4 câu hỏi tình huống:

  • Câu hỏi 1: Vì sao không nên gọi htmlspecialchars() ngay trước khi lưu vào cơ sở dữ liệu? -> Vì nó làm biến dạng dữ liệu gốc (sai lệch độ dài chuỗi và làm hỏng kết quả khi xuất ra JSON API). Chỉ gọi htmlspecialchars() ngay tại biên hiển thị ra HTML.
  • Câu hỏi 2: Khi dùng filter_var($input, FILTER_VALIDATE_INT) cho số nguyên có thể bằng 0, vì sao viết if (!filter_var(...)) lại sai? -> Vì số 0 hợp lệ khi ép kiểu boolean bằng ! sẽ bị coi nhầm là false (lỗi). Phải dùng === false.
  • Câu hỏi 3: Cờ ENT_QUOTES trong htmlspecialchars() bảo vệ thêm kịch bản nào? -> Mã hóa cả dấu nháy đơn ' bên cạnh dấu nháy kép ", ngăn thoát khỏi thuộc tính <input value='...'>.
  • Câu hỏi 4: Giá trị $_SERVER['HTTP_X_FORWARDED_FOR'] có đáng tin cậy tuyệt đối không? -> Không. Mọi khóa HTTP_* đều do client gửi lên và có thể bị giả mạo nếu không qua Reverse Proxy tin cậy lọc lại.

Hoàn thành bài học này, bạn đã biết cách tiếp nhận dữ liệu HTTP và chống XSS chuẩn mực. Hãy tiếp tục sang PHP EP 18: Cookie, Session, CSRF và băm mật khẩu trong PHP để bảo mật phiên đăng nhập và mật khẩu.

Ôn bài trước: PHP EP 16: Đọc ghi tệp, khóa tệp và xử lý JSON trong PHP. Học tiếp: PHP EP 18: Cookie, Session, CSRF và băm mật khẩu trong PHP.

Bài liên quan trong bộ PHP

Tìm hiểu thêm