Biến siêu toàn cục trong PHP thuần: $_GET, $_POST, $_FILES, $_SESSION, $_COOKIE, $_SERVER...

Khi mới viết PHP thuần (PHP core, không framework), gần như mọi trang đều đụng tới các biến bắt đầu bằng `$_`: lấy tham số trên URL, nhận dữ liệu form, upload ảnh, giữ trạng thái đăng nhập... Đó là các biến siêu toàn cục (superglobals).

Nguyễn Dương Thế Vĩ 06/10/2026 · 20 Views

Bài này đi qua từng biến hay dùng nhất, kèm ví dụ chạy được và những lỗi bảo mật thường gặp.

Lưu ý tên: biến upload file là $_FILES (có chữ S), không phải $_FILE. Viết thiếu chữ S, PHP không báo lỗi mà chỉ trả về null, nên lỗi này rất khó phát hiện.


1. Biến siêu toàn cục là gì?

Đây là các mảng PHP tạo sẵn cho mỗi request, dùng được ở mọi nơi (trong hàm, trong class) mà không cần khai báo global.

Biến Chứa gì
$_GET Tham số trên URL (query string)
$_POST Dữ liệu form gửi bằng method="post"
$_REQUEST Gộp $_GET + $_POST (+ có thể $_COOKIE)
$_FILES Thông tin file upload
$_SESSION Dữ liệu phiên, lưu trên server
$_COOKIE Cookie trình duyệt gửi lên
$_SERVER Thông tin request và môi trường server
$_ENV Biến môi trường
$GLOBALS Mọi biến toàn cục của script

Nguyên tắc vàng: mọi thứ trong $_GET, $_POST, $_FILES, $_COOKIE và phần lớn $_SERVER đều do người dùng gửi lên, nên không được tin. Luôn kiểm tra đầu vào và escape khi in ra.


2. $_GET: tham số trên URL

Với URL https://example.com/posts.php?page=2&q=php:

<?php
$page = $_GET['page'] ?? 1;   // "2" (luôn là chuỗi!)
$q    = $_GET['q'] ?? '';     // "php"

Hai điều cần nhớ:

  • Giá trị luôn là chuỗi (hoặc mảng nếu URL có dạng ?tags[]=a&tags[]=b). Muốn số thì phải ép kiểu hoặc validate.
  • Key có thể không tồn tại. Dùng ?? để tránh warning Undefined array key.

Cách validate gọn hơn là dùng filter_input:

<?php
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
    'options' => ['default' => 1, 'min_range' => 1],
]);

Khi in lại ra HTML, luôn escape để chống XSS:

<p>Kết quả tìm kiếm cho: <?= htmlspecialchars($q, ENT_QUOTES, 'UTF-8') ?></p>

Dùng $_GET cho thao tác chỉ đọc (tìm kiếm, phân trang, lọc). Đừng dùng GET để xóa hay sửa dữ liệu, vì link có thể bị bấm nhầm, bị bot crawl hoặc được nhúng vào trang khác.


3. $_POST: dữ liệu form

<form method="post" action="login.php">
    <input type="email" name="email">
    <input type="password" name="password">
    <button type="submit">Đăng nhập</button>
</form>
<?php
// login.php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method Not Allowed');
}

$email    = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';

if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
    exit('Email không hợp lệ');
}

Khi lưu vào database, luôn dùng prepared statement, không bao giờ nối chuỗi:

<?php
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

if ($user && password_verify($password, $user['password_hash'])) {
    // đăng nhập thành công
}

Nhận JSON thay vì form

$_POST chỉ được PHP điền khi body có dạng application/x-www-form-urlencoded hoặc multipart/form-data. Nếu client gửi JSON (fetch, Postman...), $_POST sẽ rỗng. Khi đó phải tự đọc body:

<?php
$data = json_decode(file_get_contents('php://input'), true) ?? [];
$email = $data['email'] ?? '';

4. $_REQUEST: nên tránh

$_REQUEST gộp nhiều nguồn lại. Thứ tự ưu tiên phụ thuộc cấu hình request_order / variables_order trong php.ini. Các file php.ini mẫu đặt request_order = "GP", tức là chỉ gộp GET và POST, và POST ghi đè GET khi trùng key.

Vấn đề là bạn không biết giá trị đến từ đâu. Một hành động lẽ ra chỉ nhận POST có thể bị kích hoạt bằng một link GET. Hãy dùng thẳng $_GET hoặc $_POST cho rõ ràng.


5. $_FILES: upload file

5.1. Form upload

Form bắt buộc có method="post" và enctype="multipart/form-data". Thiếu enctype thì $_FILES luôn rỗng.

<form method="post" action="upload.php" enctype="multipart/form-data">
    <input type="file" name="avatar" accept="image/*">
    <button type="submit">Tải lên</button>
</form>

5.2. Cấu trúc của $_FILES

$_FILES['avatar'] = [
    'name'      => 'anh-dai-dien.png',   // tên gốc từ máy người dùng (KHÔNG tin)
    'full_path' => 'anh-dai-dien.png',   // PHP 8.1+, đường dẫn khi upload cả thư mục
    'type'      => 'image/png',          // MIME do trình duyệt khai (KHÔNG tin)
    'tmp_name'  => '/tmp/phpA1b2C3',     // file tạm trên server
    'error'     => 0,                    // mã lỗi, 0 = UPLOAD_ERR_OK
    'size'      => 48213,                // kích thước (byte)
];

Các mã lỗi hay gặp:

Hằng số Giá trị Ý nghĩa
UPLOAD_ERR_OK 0 Thành công
UPLOAD_ERR_INI_SIZE 1 Vượt upload_max_filesize trong php.ini
UPLOAD_ERR_FORM_SIZE 2 Vượt MAX_FILE_SIZE khai trong form
UPLOAD_ERR_PARTIAL 3 Chỉ upload được một phần
UPLOAD_ERR_NO_FILE 4 Không chọn file
UPLOAD_ERR_NO_TMP_DIR 6 Thiếu thư mục tạm
UPLOAD_ERR_CANT_WRITE 7 Không ghi được ra đĩa
UPLOAD_ERR_EXTENSION 8 Một extension PHP chặn upload

5.3. Xử lý upload an toàn

<?php
// upload.php
$file = $_FILES['avatar'] ?? null;

if (! $file || $file['error'] !== UPLOAD_ERR_OK) {
    exit('Upload thất bại, mã lỗi: ' . ($file['error'] ?? 'không có file'));
}

// 1. Giới hạn dung lượng (2 MB).
if ($file['size'] > 2 * 1024 * 1024) {
    exit('File quá lớn (tối đa 2 MB)');
}

// 2. Kiểm tra MIME THẬT từ nội dung file, không dùng $file['type'].
$allowed = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/webp' => 'webp',
];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);

if (! isset($allowed[$mime])) {
    exit('Chỉ nhận ảnh JPG, PNG hoặc WEBP');
}

// 3. Tự đặt tên mới, KHÔNG dùng $file['name'] (tránh trùng tên, ../, file .php).
$newName = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$target  = __DIR__ . '/uploads/' . $newName;

// 4. Chỉ di chuyển bằng move_uploaded_file (nó kiểm tra file đúng là file vừa upload).
if (! move_uploaded_file($file['tmp_name'], $target)) {
    exit('Không lưu được file');
}

echo 'Đã tải lên: ' . htmlspecialchars($newName);

Bốn điều bắt buộc với upload:

  1. Không tin name và type. Cả hai do trình duyệt gửi, kẻ tấn công sửa được tùy ý.
  2. Kiểm tra MIME bằng finfo dựa trên nội dung file thật.
  3. Tự sinh tên file. Nếu giữ tên gốc, người dùng có thể upload shell.php rồi gọi thẳng nó trên trình duyệt.
  4. Chặn chạy PHP trong thư mục uploads/. Với Apache, đặt file uploads/.htaccess:
php_flag engine off
<FilesMatch "\.(php|phtml|phar)$">
    Require all denied
</FilesMatch>

Nếu host không cho dùng php_flag trong .htaccess (báo lỗi 500), bỏ dòng đầu và chỉ giữ khối FilesMatch.

5.4. Upload nhiều file

<input type="file" name="photos[]" multiple>

Lúc này $_FILES['photos'] bị "lật ngược": mỗi khóa là một mảng, ví dụ $_FILES['photos']['name'][0], $_FILES['photos']['tmp_name'][0]... Duyệt như sau:

<?php
foreach ($_FILES['photos']['error'] as $i => $error) {
    if ($error !== UPLOAD_ERR_OK) {
        continue;
    }

    $tmp  = $_FILES['photos']['tmp_name'][$i];
    $size = $_FILES['photos']['size'][$i];
    // ... kiểm tra như phần 5.3 rồi move_uploaded_file($tmp, ...)
}

5.5. Upload file lớn bị rỗng $_FILES và cả $_POST

Nếu tổng request vượt post_max_size, PHP bỏ toàn bộ body: cả $_POST lẫn $_FILES đều rỗng, không có mã lỗi nào. Kiểm tra cấu hình:

<?php
echo ini_get('upload_max_filesize'); // giới hạn mỗi file, ví dụ 2M
echo ini_get('post_max_size');       // giới hạn cả request, phải >= upload_max_filesize

6. $_SESSION: giữ trạng thái giữa các trang

HTTP không nhớ gì giữa các request. Session lưu dữ liệu trên server, còn trình duyệt chỉ giữ một cookie chứa mã phiên (mặc định tên PHPSESSID).

6.1. Bắt đầu session

<?php
// Phải gọi TRƯỚC mọi output (kể cả khoảng trắng, BOM trước thẻ <?php).
session_start();

$_SESSION['user_id'] = 42;
$_SESSION['name']    = 'Thế Vĩ';

Ở trang khác:

<?php
session_start();

if (! isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}

echo 'Xin chào ' . htmlspecialchars($_SESSION['name']);

Lỗi kinh điển Cannot send session cookie - headers already sent xảy ra vì đã echo hoặc có HTML/khoảng trắng trước session_start().

6.2. Cấu hình cookie session an toàn

<?php
session_set_cookie_params([
    'lifetime' => 0,        // hết hạn khi đóng trình duyệt
    'path'     => '/',
    'secure'   => true,     // chỉ gửi qua HTTPS
    'httponly' => true,     // JavaScript không đọc được (chống đánh cắp qua XSS)
    'samesite' => 'Lax',    // giảm rủi ro CSRF
]);
session_start();

6.3. Đăng nhập: đổi mã phiên

Sau khi đăng nhập thành công, luôn tạo mã phiên mới để chống session fixation:

<?php
if ($user && password_verify($password, $user['password_hash'])) {
    session_regenerate_id(true); // true = xóa phiên cũ
    $_SESSION['user_id'] = $user['id'];
    header('Location: dashboard.php');
    exit;
}

6.4. Đăng xuất đúng cách

<?php
session_start();

$_SESSION = [];                          // xóa dữ liệu trong phiên

$params = session_get_cookie_params();   // xóa cookie phiên trên trình duyệt
setcookie(session_name(), '', [
    'expires'  => time() - 3600,
    'path'     => $params['path'],
    'domain'   => $params['domain'],
    'secure'   => $params['secure'],
    'httponly' => $params['httponly'],
    'samesite' => $params['samesite'],
]);

session_destroy();                       // xóa file phiên trên server

header('Location: login.php');
exit;

6.5. Flash message: thông báo hiện một lần

<?php
// Sau khi lưu bài viết:
$_SESSION['flash'] = 'Đã lưu bài viết';
header('Location: posts.php');
exit;
<?php
// posts.php: đọc xong thì xóa ngay
if (isset($_SESSION['flash'])) {
    echo '<div class="alert">' . htmlspecialchars($_SESSION['flash']) . '</div>';
    unset($_SESSION['flash']);
}

6.6. Chống CSRF bằng token trong session

<?php
// Tạo token một lần cho phiên
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
?>
<form method="post" action="delete.php">
    <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
    <input type="hidden" name="id" value="5">
    <button>Xóa</button>
</form>
<?php
// delete.php
session_start();

if (! hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(419);
    exit('Phiên không hợp lệ, vui lòng tải lại trang');
}

hash_equals so sánh trong thời gian cố định, tránh tấn công đoán token theo thời gian phản hồi.


7. $_COOKIE: dữ liệu lưu trên trình duyệt

Cookie lưu ở máy người dùng, nên họ xem và sửa được. Chỉ dùng cookie cho dữ liệu không nhạy cảm như ngôn ngữ hay giao diện sáng/tối.

7.1. Tạo cookie

<?php
setcookie('theme', 'dark', [
    'expires'  => time() + 60 * 60 * 24 * 30, // 30 ngày
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

Giống session_start(), setcookie() gửi header nên phải gọi trước mọi output.

7.2. Đọc cookie

<?php
$theme = $_COOKIE['theme'] ?? 'light';

// Cookie do người dùng sửa được -> chỉ chấp nhận giá trị trong danh sách cho phép
if (! in_array($theme, ['light', 'dark'], true)) {
    $theme = 'light';
}

Bẫy hay gặp: cookie vừa setcookie() chưa có trong $_COOKIE ở cùng request đó. Nó chỉ xuất hiện từ request sau, khi trình duyệt gửi lại.

7.3. Xóa cookie

Đặt thời gian hết hạn trong quá khứ, với cùng path và domain lúc tạo:

<?php
setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);

Session hay Cookie?

$_SESSION $_COOKIE
Lưu ở đâu Server Trình duyệt
Người dùng sửa được? Không Có
Dung lượng Lớn tùy server Khoảng 4 KB mỗi cookie
Dùng cho Đăng nhập, giỏ hàng, CSRF token Ngôn ngữ, theme, "ghi nhớ" lựa chọn

8. $_SERVER: thông tin request và server

Các khóa hay dùng:

<?php
$_SERVER['REQUEST_METHOD'];  // "GET", "POST", ...
$_SERVER['REQUEST_URI'];     // "/posts.php?page=2"
$_SERVER['QUERY_STRING'];    // "page=2"
$_SERVER['HTTP_HOST'];       // "example.com"
$_SERVER['HTTPS'];           // "on" khi dùng HTTPS (có thể không tồn tại)
$_SERVER['REMOTE_ADDR'];     // IP kết nối tới server
$_SERVER['HTTP_USER_AGENT']; // trình duyệt
$_SERVER['HTTP_REFERER'];    // trang trước đó (có thể không có)
$_SERVER['SCRIPT_NAME'];     // "/posts.php"
$_SERVER['DOCUMENT_ROOT'];   // thư mục gốc của web

Ví dụ phân nhánh theo method:

<?php
match ($_SERVER['REQUEST_METHOD']) {
    'GET'   => showForm(),
    'POST'  => handleSubmit(),
    default => http_response_code(405),
};

Cẩn thận:

  • Mọi khóa bắt đầu bằng HTTP_ là header do client gửi, giả mạo được. HTTP_HOST, HTTP_REFERER, HTTP_USER_AGENT đều phải escape trước khi in ra.
  • HTTP_X_FORWARDED_FOR hay được dùng để lấy "IP thật", nhưng ai cũng gửi được header này. Chỉ tin nó khi website chạy sau một proxy/CDN do bạn kiểm soát.
  • PHP_SELF có thể chứa chuỗi do người dùng chèn vào URL. Đừng in thẳng $_SERVER['PHP_SELF'] vào action của form.

9. $_ENV và getenv(): biến môi trường

Dùng cho cấu hình nhạy cảm như mật khẩu database hay API key, để không ghi cứng vào code.

<?php
$dbPass = getenv('DB_PASSWORD');      // thường hoạt động
$dbPass = $_ENV['DB_PASSWORD'] ?? ''; // hay bị rỗng!

$_ENV thường rỗng vì php.ini mặc định đặt variables_order = "GPCS" (không có chữ E). Vì vậy getenv() đáng tin hơn. Trên shared hosting không đặt được biến môi trường, có thể dùng một file config.php nằm ngoài thư mục public:

<?php
// config.php (đặt ngoài public_html)
return [
    'db_host' => 'localhost',
    'db_pass' => 'mat-khau-bi-mat',
];
<?php
$config = require __DIR__ . '/../config.php';

10. $GLOBALS: truy cập biến toàn cục

<?php
$siteName = 'The Vi Blog';

function title(string $page): string
{
    return $page . ' | ' . $GLOBALS['siteName'];
}

echo title('Trang chủ'); // "Trang chủ | The Vi Blog"

Từ PHP 8.1, không gán đè được cả mảng $GLOBALS = [...] nữa, nhưng vẫn đọc và ghi từng phần tử bình thường.

Dù vậy, nên hạn chế dùng: hàm phụ thuộc biến toàn cục rất khó test và khó lần ra ai đã sửa giá trị. Truyền giá trị qua tham số hàm vẫn tốt hơn.


11. Ví dụ tổng hợp: form đăng bài có ảnh

Ghép mọi thứ ở trên: session (đăng nhập + CSRF + flash), POST (dữ liệu form), FILES (ảnh), SERVER (method).

<?php
// create-post.php
session_start();

if (! isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}

$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (! hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
        exit('CSRF token không hợp lệ');
    }

    $title = trim($_POST['title'] ?? '');
    if ($title === '' || mb_strlen($title) > 200) {
        $errors[] = 'Tiêu đề bắt buộc, tối đa 200 ký tự';
    }

    $thumbnail = null;
    $file = $_FILES['thumbnail'] ?? null;

    if ($file && $file['error'] !== UPLOAD_ERR_NO_FILE) {
        $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
        $mime = $file['error'] === UPLOAD_ERR_OK
            ? (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name'])
            : null;

        if (! isset($allowed[$mime]) || $file['size'] > 2 * 1024 * 1024) {
            $errors[] = 'Ảnh phải là JPG/PNG/WEBP và nhỏ hơn 2 MB';
        } else {
            $thumbnail = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
            move_uploaded_file($file['tmp_name'], __DIR__ . '/uploads/' . $thumbnail);
        }
    }

    if (! $errors) {
        $stmt = $pdo->prepare('INSERT INTO posts (user_id, title, thumbnail) VALUES (?, ?, ?)');
        $stmt->execute([$_SESSION['user_id'], $title, $thumbnail]);

        $_SESSION['flash'] = 'Đã đăng bài';
        header('Location: posts.php'); // Post/Redirect/Get: F5 không gửi lại form
        exit;
    }
}
?>
<?php foreach ($errors as $error): ?>
    <p class="error"><?= htmlspecialchars($error) ?></p>
<?php endforeach; ?>

<form method="post" enctype="multipart/form-data">
    <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
    <input name="title" value="<?= htmlspecialchars($_POST['title'] ?? '') ?>">
    <input type="file" name="thumbnail" accept="image/*">
    <button type="submit">Đăng bài</button>
</form>

Mẫu Post/Redirect/Get ở cuối rất đáng nhớ: sau khi xử lý POST thành công thì redirect, để người dùng bấm F5 không bị gửi lại form và tạo bài trùng.


12. Checklist bảo mật nhanh

  • Dùng ?? hoặc isset() trước khi đọc key, tránh warning.
  • Validate kiểu dữ liệu: filter_input, filter_var, ép kiểu, danh sách giá trị cho phép.
  • Escape khi in ra HTML: htmlspecialchars($x, ENT_QUOTES, 'UTF-8').
  • Truy vấn database bằng prepared statement (PDO), không nối chuỗi.
  • Upload: kiểm tra error, size, MIME bằng finfo, tự đặt tên, dùng move_uploaded_file, chặn chạy PHP trong thư mục upload.
  • Session: cookie httponly + secure + samesite, gọi session_regenerate_id(true) sau đăng nhập.
  • Mọi form thay đổi dữ liệu đều có CSRF token, so sánh bằng hash_equals.
  • Không lưu thông tin nhạy cảm trong cookie.
  • Không dùng $_REQUEST; dùng đúng $_GET hoặc $_POST.
  • Gọi session_start(), setcookie(), header() trước mọi output.

Kết luận

Các biến siêu toàn cục là "cửa ngõ" để dữ liệu từ người dùng đi vào ứng dụng PHP. Dùng chúng thì dễ; dùng an toàn mới là phần quan trọng. Nắm vững chúng cũng giúp bạn hiểu framework hơn: $request->input(), $request->file(), session() hay cookie() trong Laravel thực chất được xây dựng bên trên chính các biến này, và đã làm sẵn phần lớn các bước kiểm tra bên trên cho bạn.