Tổng hợp Upload File trong PHP

Tổng hợp Upload File trong PHP

Tổng hợp cách upload file trong PHP từ cơ bản đến nâng cao, gồm xử lý $_FILES, kiểm tra định dạng, dung lượng, bảo mật và lưu trữ file an toàn, chi tiết như nào hãy cùng chúng tôi phân tích:

Upload file là một trong những chức năng xuất hiện rất thường xuyên trong các website PHP. Từ form đăng ký tài khoản có ảnh đại diện, trang quản trị cho phép tải tài liệu, hệ thống đăng bài có ảnh minh họa cho đến các ứng dụng cần nhận file PDF, Word hoặc Excel, tất cả đều có chung một bài toán: tiếp nhận file từ máy người dùng, kiểm tra dữ liệu, lưu file an toàn và sử dụng lại khi cần.

Nhìn bề ngoài, việc upload file trong PHP có vẻ đơn giản. Chỉ cần tạo một biểu mẫu HTML, đặt thuộc tính enctype="multipart/form-data" rồi xử lý dữ liệu bằng $_FILES. Tuy nhiên, một hệ thống upload thực tế không nên chỉ dừng ở việc "nhận file rồi lưu vào thư mục". Nếu không kiểm tra đúng cách, website có thể gặp các vấn đề như file quá lớn, phần mở rộng giả mạo, tên file chứa ký tự bất thường, file không được tải đầy đủ, ghi đè dữ liệu cũ hoặc nghiêm trọng hơn là tạo ra lỗ hổng bảo mật.

Bài viết này tổng hợp cách xử lý upload file trong PHP từ cơ bản đến nâng cao, tập trung vào cách tư duy khi xây dựng chức năng upload thay vì chỉ đưa ra một đoạn code có thể chạy. Bạn có thể sử dụng từng phần độc lập hoặc kết hợp chúng để tạo thành một quy trình upload hoàn chỉnh cho website.

Tổng hợp Upload File trong PHP

Upload file trong PHP hoạt động như thế nào?

Khi người dùng chọn một file trên máy tính và nhấn nút gửi form, trình duyệt sẽ đóng gói dữ liệu rồi gửi đến máy chủ thông qua HTTP request. PHP tiếp nhận request này và cung cấp thông tin về file thông qua biến siêu toàn cục $_FILES.

Điểm quan trọng là file được upload không phải ngay lập tức trở thành một file cố định trong thư mục website. Trong quá trình xử lý request, PHP sẽ tiếp nhận dữ liệu upload và lưu tạm trên máy chủ. Lập trình viên cần kiểm tra file, sau đó sử dụng hàm move_uploaded_file() để chuyển file đến vị trí lưu trữ mong muốn.

Một quy trình upload cơ bản thường có thể hình dung như sau:

  1. Người dùng chọn file từ thiết bị.
  2. Form gửi file đến máy chủ bằng phương thức POST.
  3. PHP tiếp nhận file thông qua $_FILES.
  4. Chương trình kiểm tra lỗi upload.
  5. Kiểm tra kích thước và loại file.
  6. Tạo tên file mới an toàn.
  7. Di chuyển file vào thư mục lưu trữ.
  8. Lưu thông tin file vào cơ sở dữ liệu nếu hệ thống cần quản lý.
  9. Trả kết quả thành công hoặc thông báo lỗi cho người dùng.

Việc chia quy trình thành nhiều bước giúp code dễ kiểm soát hơn. Đặc biệt, không nên coi việc kiểm tra file là một bước phụ. Đây là phần quyết định hệ thống upload có ổn định và an toàn hay không.

Form HTML dùng để gửi file lên PHP

Để trình duyệt có thể gửi dữ liệu file, form cần sử dụng phương thức POST và thuộc tính enctype="multipart/form-data". Nếu thiếu thuộc tính này, PHP thường sẽ không nhận được file thông qua $_FILES như mong đợi.

Một form upload đơn giản có thể viết như sau:

<form action="upload.php" method="post" enctype="multipart/form-data">
    <label for="document">Chọn file:</label>
    <input type="file" name="document" id="document">
    <button type="submit">Upload file</button>
</form>

Ở đây, giá trị name="document" rất quan trọng vì PHP sẽ sử dụng chính tên này để tạo khóa tương ứng trong $_FILES. Ví dụ, nếu input có tên là document, dữ liệu file sẽ được truy cập bằng $_FILES['document'].

Nếu form có nhiều loại file khác nhau, mỗi input có thể sử dụng một tên riêng. Tuy nhiên, trong các hệ thống lớn, nên đặt tên trường dữ liệu nhất quán để phần xử lý phía PHP dễ bảo trì.

Giải thích cấu trúc của $_FILES

Đối với một file được upload, PHP cung cấp một mảng chứa nhiều thông tin liên quan. Đây là phần mà người mới học PHP cần hiểu rõ trước khi viết logic xử lý.

Với input có tên document, dữ liệu thường được truy cập qua các khóa sau:

  • $_FILES['document']['name']: tên file gốc do trình duyệt gửi lên.
  • $_FILES['document']['type']: MIME type do trình duyệt cung cấp.
  • $_FILES['document']['tmp_name']: đường dẫn file tạm trên máy chủ.
  • $_FILES['document']['error']: mã lỗi trong quá trình upload.
  • $_FILES['document']['size']: kích thước file tính theo byte.

Ví dụ, bạn có thể kiểm tra thông tin của file bằng đoạn PHP sau:

<?php

if (isset($_FILES['document'])) {
    echo $_FILES['document']['name'];
    echo $_FILES['document']['type'];
    echo $_FILES['document']['tmp_name'];
    echo $_FILES['document']['error'];
    echo $_FILES['document']['size'];
}

Tuy nhiên, không nên lấy các giá trị này và lưu trực tiếp mà chưa kiểm tra. Đặc biệt, type không nên được xem là nguồn xác thực duy nhất vì thông tin MIME type gửi từ phía client có thể không đáng tin cậy.

Các mã lỗi thường gặp khi upload

Khóa error trong $_FILES giúp xác định trạng thái của quá trình upload. Một số mã thường gặp gồm:

  • UPLOAD_ERR_OK: file được upload thành công.
  • UPLOAD_ERR_INI_SIZE: file vượt quá giới hạn được cấu hình bởi PHP.
  • UPLOAD_ERR_FORM_SIZE: file vượt quá giới hạn do form quy định.
  • UPLOAD_ERR_PARTIAL: file chỉ được upload một phần.
  • UPLOAD_ERR_NO_FILE: người dùng không chọn file.
  • UPLOAD_ERR_NO_TMP_DIR: máy chủ không có thư mục tạm để xử lý.
  • UPLOAD_ERR_CANT_WRITE: không thể ghi file xuống ổ đĩa.
  • UPLOAD_ERR_EXTENSION: một extension PHP đã dừng quá trình upload.

Thay vì chỉ kiểm tra xem $_FILES có tồn tại hay không, nên kiểm tra mã lỗi một cách rõ ràng. Điều này giúp người dùng nhận được thông báo phù hợp và giúp lập trình viên dễ xác định nguyên nhân khi hệ thống gặp sự cố.

<?php

if (!isset($_FILES['document'])) {
    exit('Chưa có file được gửi lên.');
}

if ($_FILES['document']['error'] !== UPLOAD_ERR_OK) {
    exit('Upload file không thành công.');
}

echo 'File đã được tiếp nhận.';

Trong ứng dụng thực tế, bạn có thể ánh xạ từng mã lỗi thành thông báo cụ thể thay vì sử dụng một thông báo chung cho mọi trường hợp.

Kiểm tra người dùng có chọn file hay chưa

Một lỗi phổ biến là người dùng nhấn nút upload nhưng không chọn file. Trường hợp này cần được xử lý trước khi chương trình truy cập sâu vào các thông tin khác.

Có thể kiểm tra bằng cách kết hợp isset() và mã lỗi UPLOAD_ERR_NO_FILE.

<?php

if (!isset($_FILES['document'])) {
    exit('Không tìm thấy dữ liệu upload.');
}

if ($_FILES['document']['error'] === UPLOAD_ERR_NO_FILE) {
    exit('Vui lòng chọn một file trước khi upload.');
}

if ($_FILES['document']['error'] !== UPLOAD_ERR_OK) {
    exit('Đã xảy ra lỗi trong quá trình upload.');
}

echo 'Có file hợp lệ để tiếp tục xử lý.';

Cách này rõ ràng hơn so với việc chỉ kiểm tra tên file. Trong một số tình huống, tên file có thể không phản ánh chính xác trạng thái upload.

Giới hạn kích thước file

Cho phép người dùng upload file không giới hạn là một quyết định rủi ro. Một file quá lớn có thể làm tăng mức sử dụng ổ đĩa, bộ nhớ hoặc băng thông. Nếu hệ thống cho phép nhiều người upload cùng lúc, vấn đề càng nghiêm trọng hơn.

PHP có những giới hạn liên quan đến upload được cấu hình trong php.ini. Một số thiết lập quan trọng gồm upload_max_filesizepost_max_size.

Ví dụ, bạn có thể đặt giới hạn ứng dụng ở mức 5 MB:

<?php

$maxSize = 5 * 1024 * 1024;

if ($_FILES['document']['size'] > $maxSize) {
    exit('File không được vượt quá 5 MB.');
}

Điều cần lưu ý là giới hạn trong code PHP không thể thay thế hoàn toàn cấu hình máy chủ. Nếu file bị chặn ngay từ tầng PHP do vượt upload_max_filesize hoặc post_max_size, chương trình có thể không nhận được dữ liệu theo cách bạn mong muốn.

Vì vậy, khi xây dựng chức năng upload, nên xác định rõ ba lớp giới hạn: giới hạn của máy chủ, giới hạn của PHP và giới hạn riêng của nghiệp vụ.

Kiểm tra phần mở rộng của file

Phần mở rộng như JPG, PNG, PDF hay ZIP thường được dùng để xác định loại file mà hệ thống muốn cho phép. Tuy nhiên, chỉ kiểm tra phần mở rộng là chưa đủ để bảo vệ website.

Ví dụ, một file có tên anh.jpg không đồng nghĩa với việc nội dung thực sự bên trong là một hình ảnh JPEG. Người dùng có thể đổi tên một file khác thành .jpg.

Dù vậy, việc kiểm tra phần mở rộng vẫn cần thiết để kiểm soát phạm vi file được phép upload.

<?php

$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];

$originalName = $_FILES['document']['name'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

if (!in_array($extension, $allowedExtensions, true)) {
    exit('Định dạng file không được hỗ trợ.');
}

echo 'Phần mở rộng được chấp nhận.';

Danh sách cho phép nên được xây dựng theo nhu cầu thực tế. Không nên cho phép tất cả phần mở rộng chỉ vì muốn hệ thống "linh hoạt". Một hệ thống upload tốt thường áp dụng nguyên tắc chỉ cho phép những gì thực sự cần.

Kiểm tra MIME type bằng finfo

Để kiểm tra nội dung file đáng tin cậy hơn, PHP cung cấp thư viện Fileinfo. Hàm finfo_file() có thể giúp xác định MIME type dựa trên nội dung file thay vì chỉ dựa vào thông tin do trình duyệt gửi lên.

Ví dụ:

<?php

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['document']['tmp_name']);

$allowedMimeTypes = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'application/pdf'
];

if (!in_array($mimeType, $allowedMimeTypes, true)) {
    exit('Nội dung file không thuộc định dạng được cho phép.');
}

echo 'MIME type hợp lệ: ' . $mimeType;

Đây là cách tiếp cận tốt hơn việc chỉ tin vào $_FILES['document']['type']. Tuy nhiên, trong các hệ thống yêu cầu bảo mật cao, vẫn nên kết hợp nhiều lớp xác thực thay vì phụ thuộc vào một kiểm tra duy nhất.

Đổi tên file trước khi lưu

Một trong những nguyên tắc quan trọng khi upload là không nên dùng nguyên tên file của người dùng làm tên file vật lý trên máy chủ.

Tên file do người dùng gửi có thể chứa khoảng trắng, ký tự đặc biệt hoặc những chuỗi gây khó khăn cho việc xử lý. Ngoài ra, nếu hai người cùng upload một file có cùng tên, file sau có thể ghi đè file trước nếu hệ thống không xử lý đúng.

Giải pháp phổ biến là tạo tên mới bằng mã ngẫu nhiên hoặc UUID rồi giữ lại phần mở rộng hợp lệ.

<?php

$extension = strtolower(
    pathinfo($_FILES['document']['name'], PATHINFO_EXTENSION)
);

$newName = bin2hex(random_bytes(16)) . '.' . $extension;

echo $newName;

Cách tạo tên mới này giúp giảm khả năng trùng tên và không phụ thuộc vào tên file gốc. Nếu file được lưu trên hệ thống lớn, bạn có thể xây dựng thêm cấu trúc thư mục theo ngày, tháng hoặc mã người dùng để tránh một thư mục chứa quá nhiều file.

Di chuyển file bằng move_uploaded_file()

Sau khi file vượt qua các bước kiểm tra cần thiết, move_uploaded_file() được sử dụng để chuyển file từ vị trí tạm đến thư mục lưu trữ.

Ví dụ đơn giản:

<?php

$uploadDir = __DIR__ . '/uploads/';

if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

$extension = strtolower(
    pathinfo($_FILES['document']['name'], PATHINFO_EXTENSION)
);

$newName = bin2hex(random_bytes(16)) . '.' . $extension;
$destination = $uploadDir . $newName;

if (move_uploaded_file($_FILES['document']['tmp_name'], $destination)) {
    echo 'Upload thành công.';
} else {
    echo 'Không thể lưu file.';
}

Hàm này có ý nghĩa quan trọng vì PHP kiểm tra xem nguồn file có thực sự là file được upload thông qua HTTP POST hay không. Trong ứng dụng thực tế, vẫn cần kết hợp với các bước kiểm tra khác để tạo thành quy trình an toàn.

Tạo quy trình upload file hoàn chỉnh

Một đoạn code upload hoàn chỉnh nên xử lý theo thứ tự hợp lý. Không nên di chuyển file ngay khi vừa nhận được dữ liệu. Hãy kiểm tra lỗi, kích thước, phần mở rộng, MIME type rồi mới tạo tên và lưu file.

Ví dụ dưới đây minh họa một quy trình upload cơ bản dành cho file ảnh:

<?php

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    exit('Phương thức request không hợp lệ.');
}

if (!isset($_FILES['image'])) {
    exit('Không tìm thấy file.');
}

$file = $_FILES['image'];

if ($file['error'] !== UPLOAD_ERR_OK) {
    exit('Upload thất bại.');
}

$maxSize = 5 * 1024 * 1024;

if ($file['size'] > $maxSize) {
    exit('Ảnh không được vượt quá 5 MB.');
}

$allowedMimeTypes = [
    'image/jpeg',
    'image/png',
    'image/gif',
    'image/webp'
];

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);

if (!in_array($mimeType, $allowedMimeTypes, true)) {
    exit('Định dạng ảnh không được hỗ trợ.');
}

$extensionMap = [
    'image/jpeg' => 'jpg',
    'image/png' => 'png',
    'image/gif' => 'gif',
    'image/webp' => 'webp'
];

$extension = $extensionMap[$mimeType];
$newName = bin2hex(random_bytes(16)) . '.' . $extension;

$uploadDir = __DIR__ . '/uploads/';

if (!is_dir($uploadDir)) {
    if (!mkdir($uploadDir, 0755, true)) {
        exit('Không thể tạo thư mục lưu ảnh.');
    }
}

$destination = $uploadDir . $newName;

if (!move_uploaded_file($file['tmp_name'], $destination)) {
    exit('Không thể lưu ảnh lên máy chủ.');
}

echo 'Ảnh đã được upload thành công.';

Điểm đáng chú ý trong ví dụ này là phần mở rộng không lấy trực tiếp từ tên file gốc. Thay vào đó, chương trình xác định MIME type rồi ánh xạ MIME type sang phần mở rộng tương ứng. Cách làm này giúp giảm sự phụ thuộc vào tên file mà người dùng cung cấp.

Upload nhiều file cùng lúc

Trong nhiều hệ thống, người dùng cần chọn nhiều file trong một lần. HTML hỗ trợ trường hợp này bằng thuộc tính multiple.

<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="documents[]" multiple>
    <button type="submit">Upload nhiều file</button>
</form>

Khi sử dụng documents[], PHP sẽ tổ chức dữ liệu trong $_FILES['documents'] theo từng mảng thông tin. Lập trình viên cần lặp qua chỉ số của từng file để xử lý.

<?php

if (!isset($_FILES['documents'])) {
    exit('Không có file nào được gửi.');
}

$totalFiles = count($_FILES['documents']['name']);

for ($i = 0; $i < $totalFiles; $i++) {
    if ($_FILES['documents']['error'][$i] !== UPLOAD_ERR_OK) {
        continue;
    }

    $tmpName = $_FILES['documents']['tmp_name'][$i];
    $originalName = $_FILES['documents']['name'][$i];
    $size = $_FILES['documents']['size'][$i];

    echo 'Đang xử lý: ' . $originalName . '<br>';
    echo 'Kích thước: ' . $size . ' byte<br>';
}

Khi xử lý nhiều file, nên đặt giới hạn số lượng file trong mỗi lần upload. Nếu không, người dùng có thể gửi quá nhiều file khiến máy chủ phải xử lý một request lớn hơn dự kiến.

Giới hạn số lượng file khi upload nhiều

Ngoài kích thước từng file, hệ thống nên kiểm soát tổng số file. Ví dụ, một form có thể cho phép tối đa 10 file mỗi lần.

<?php

$maxFiles = 10;
$totalFiles = count($_FILES['documents']['name']);

if ($totalFiles > $maxFiles) {
    exit('Bạn chỉ được upload tối đa 10 file mỗi lần.');
}

Trong thực tế, bạn có thể kết hợp giới hạn số lượng với tổng dung lượng. Điều này giúp kiểm soát tài nguyên tốt hơn, đặc biệt với các website có nhiều người dùng cùng upload.

Xử lý tên file an toàn

Một lỗi thường gặp là sử dụng trực tiếp tên file gốc để tạo đường dẫn.

Ví dụ, cách làm dưới đây không nên được sử dụng:

<?php

$destination = 'uploads/' . $_FILES['document']['name'];

move_uploaded_file(
    $_FILES['document']['tmp_name'],
    $destination
);

Vấn đề không chỉ nằm ở ký tự đặc biệt. Tên file do người dùng cung cấp không nên được coi là một phần đáng tin cậy của đường dẫn hệ thống.

Thay vào đó, hãy tạo tên vật lý mới bằng giá trị ngẫu nhiên. Nếu muốn hiển thị tên gốc cho người dùng, bạn có thể lưu tên gốc vào cơ sở dữ liệu như một thông tin mô tả, còn file vật lý sử dụng tên riêng do hệ thống tạo.

Lưu thông tin file vào cơ sở dữ liệu

Đối với website đơn giản, chỉ lưu file vào thư mục có thể là đủ. Nhưng khi hệ thống cần quản lý hàng nghìn hoặc hàng triệu file, việc lưu metadata vào cơ sở dữ liệu sẽ thuận tiện hơn.

Một bảng quản lý file có thể chứa các thông tin như:

  • ID của file.
  • Tên file gốc.
  • Tên file vật lý.
  • MIME type.
  • Kích thước file.
  • Đường dẫn hoặc khóa định danh file.
  • ID người upload.
  • Thời gian upload.

Ví dụ, người dùng upload file có tên bao-cao.pdf. Hệ thống có thể lưu file vật lý với tên ngẫu nhiên như a8f4c2....pdf, đồng thời lưu tên hiển thị bao-cao.pdf vào cơ sở dữ liệu.

Cách tách biệt tên hiển thị và tên vật lý giúp hệ thống linh hoạt hơn. Người dùng vẫn nhìn thấy tên quen thuộc trong giao diện, trong khi máy chủ không phụ thuộc vào tên file do client gửi lên.

Upload ảnh và kiểm tra file có thực sự là hình ảnh

Nếu chức năng chỉ nhận ảnh, ngoài MIME type bạn có thể kiểm tra thông tin hình ảnh bằng các hàm xử lý ảnh của PHP. Đây là một lớp xác minh bổ sung hữu ích khi website cho phép người dùng tải ảnh đại diện hoặc ảnh bài viết.

Ví dụ:

<?php

$imageInfo = getimagesize($_FILES['image']['tmp_name']);

if ($imageInfo === false) {
    exit('File được gửi lên không phải hình ảnh hợp lệ.');
}

$width = $imageInfo[0];
$height = $imageInfo[1];

echo 'Chiều rộng: ' . $width;
echo 'Chiều cao: ' . $height;

Cách kiểm tra này cũng cho phép hệ thống đặt giới hạn về kích thước hình ảnh. Chẳng hạn, một website có thể từ chối ảnh có chiều rộng hoặc chiều cao vượt quá mức cho phép để tránh việc xử lý những ảnh quá lớn.

Kiểm tra kích thước ảnh

Kích thước file tính bằng byte và kích thước hình ảnh tính bằng pixel là hai vấn đề khác nhau. Một ảnh có dung lượng nhỏ chưa chắc có kích thước hiển thị nhỏ, và ngược lại.

Nếu website chỉ cần ảnh đại diện, có thể giới hạn chiều rộng và chiều cao:

<?php

$imageInfo = getimagesize($_FILES['image']['tmp_name']);

if ($imageInfo === false) {
    exit('File không phải ảnh hợp lệ.');
}

$maxWidth = 4000;
$maxHeight = 4000;

if ($imageInfo[0] > $maxWidth || $imageInfo[1] > $maxHeight) {
    exit('Kích thước ảnh vượt quá giới hạn cho phép.');
}

Đối với hệ thống chuyên nghiệp, ảnh gốc có thể được lưu riêng và tạo thêm nhiều phiên bản thumbnail hoặc ảnh tối ưu để phục vụ giao diện. Điều này giúp giảm tải cho website khi hiển thị danh sách nhiều ảnh.

Không nên chỉ dựa vào phần mở rộng

Kiểm tra phần mở rộng là cần thiết nhưng không nên là lớp bảo vệ duy nhất. Một người dùng có thể đổi tên một file từ định dạng này sang định dạng khác mà không thay đổi nội dung bên trong.

Ví dụ, việc thấy tên file kết thúc bằng .jpg chỉ cho biết tên file có phần mở rộng JPG. Nó không chứng minh nội dung thực tế là ảnh JPEG.

Một quy trình tốt hơn là kết hợp:

  • Kiểm tra mã lỗi upload.
  • Giới hạn kích thước file.
  • Kiểm tra phần mở rộng theo danh sách cho phép.
  • Xác định MIME type từ nội dung file.
  • Đối với ảnh, kiểm tra bằng công cụ xử lý ảnh.
  • Đổi tên file bằng tên do hệ thống tạo.
  • Lưu file tại vị trí phù hợp.

Không có một phép kiểm tra đơn lẻ nào có thể giải quyết toàn bộ vấn đề. Bảo mật upload hiệu quả đến từ việc xếp nhiều lớp kiểm tra theo đúng thứ tự.

Không cho phép upload tùy ý các file thực thi

Nếu website cho phép người dùng upload file PHP hoặc các file có khả năng được máy chủ thực thi, rủi ro bảo mật có thể rất nghiêm trọng. Vì vậy, các hệ thống upload công khai thường không cho phép những định dạng có thể trở thành mã thực thi trên server.

Đặc biệt, không nên xây dựng danh sách cho phép theo kiểu "cho phép tất cả rồi chặn một vài phần mở rộng nguy hiểm". Cách an toàn hơn là xác định rõ chức năng cần nhận loại file nào và chỉ cho phép đúng những định dạng đó.

Nếu website chỉ cần ảnh, hãy chỉ cho phép các định dạng ảnh cần thiết. Nếu hệ thống nhận tài liệu, hãy giới hạn vào những loại tài liệu thực sự được hỗ trợ.

Lưu file bên ngoài thư mục public

Trong những hệ thống cần mức bảo mật cao, một lựa chọn đáng cân nhắc là lưu file upload bên ngoài thư mục có thể truy cập trực tiếp từ trình duyệt.

Thay vì để người dùng truy cập trực tiếp vào đường dẫn file, website có thể tạo một endpoint PHP để kiểm tra quyền rồi mới đọc và trả file về.

Ví dụ, quy trình có thể là:

  1. Người dùng yêu cầu tải file.
  2. PHP xác định người dùng hiện tại.
  3. Kiểm tra người dùng có quyền truy cập file hay không.
  4. Nếu được phép, PHP đọc file từ vùng lưu trữ.
  5. Trả nội dung file về trình duyệt.

Cách này đặc biệt hữu ích đối với tài liệu riêng tư, hóa đơn, hợp đồng hoặc dữ liệu nội bộ. Người biết đường dẫn không thể tự động truy cập file nếu chưa vượt qua bước kiểm tra quyền.

Kiểm soát quyền thư mục lưu trữ

Thư mục upload cần được cấu hình quyền phù hợp. Không nên cấp quyền ghi quá rộng cho toàn bộ hệ thống chỉ để giải quyết vấn đề upload.

Nguyên tắc nên áp dụng là cấp quyền tối thiểu cần thiết cho tiến trình web server. Đồng thời, nên phân biệt thư mục chứa file người dùng với thư mục chứa mã nguồn ứng dụng.

Nếu website có thể cấu hình máy chủ, việc ngăn máy chủ thực thi các file được upload trong thư mục lưu trữ cũng là một lớp phòng vệ quan trọng. Cấu hình cụ thể phụ thuộc vào web server đang sử dụng như Apache hoặc Nginx.

Xử lý upload bằng hàm riêng

Khi website có nhiều nơi cần upload, không nên sao chép cùng một đoạn code ở mọi trang. Một giải pháp tốt hơn là tạo hàm xử lý upload dùng chung.

Ví dụ đơn giản:

<?php

function uploadFile(
    array $file,
    string $uploadDir,
    array $allowedMimeTypes,
    int $maxSize
): string {

    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Upload file thất bại.');
    }

    if ($file['size'] > $maxSize) {
        throw new RuntimeException('File vượt quá kích thước cho phép.');
    }

    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mimeType = $finfo->file($file['tmp_name']);

    if (!isset($allowedMimeTypes[$mimeType])) {
        throw new RuntimeException('Định dạng file không được hỗ trợ.');
    }

    $extension = $allowedMimeTypes[$mimeType];
    $fileName = bin2hex(random_bytes(16)) . '.' . $extension;

    if (!is_dir($uploadDir)) {
        if (!mkdir($uploadDir, 0755, true)) {
            throw new RuntimeException('Không thể tạo thư mục upload.');
        }
    }

    $destination = rtrim($uploadDir, DIRECTORY_SEPARATOR)
        . DIRECTORY_SEPARATOR
        . $fileName;

    if (!move_uploaded_file($file['tmp_name'], $destination)) {
        throw new RuntimeException('Không thể lưu file.');
    }

    return $fileName;
}

Với cách này, các trang khác nhau chỉ cần gọi hàm với cấu hình phù hợp. Tuy nhiên, trong dự án thực tế, hàm có thể được mở rộng để xử lý logging, tạo thư mục theo ngày, lưu metadata, kiểm tra quyền hoặc tích hợp dịch vụ lưu trữ bên ngoài.

Xử lý lỗi upload thân thiện với người dùng

Thông báo "Upload thất bại" thường không đủ hữu ích. Người dùng cần biết họ phải làm gì tiếp theo. Chẳng hạn, nếu file vượt quá dung lượng, thông báo nên nói rõ giới hạn là bao nhiêu.

Một hệ thống tốt nên phân biệt các trường hợp như:

  • Chưa chọn file.
  • File quá lớn.
  • Định dạng không được hỗ trợ.
  • File bị upload không hoàn chỉnh.
  • Máy chủ không thể ghi file.
  • Thư mục lưu trữ không tồn tại hoặc không có quyền ghi.
  • File không vượt qua kiểm tra nội dung.

Đối với người dùng cuối, thông báo nên ngắn gọn. Đối với quản trị viên hoặc hệ thống log, nên lưu thông tin chi tiết hơn để phục vụ việc điều tra lỗi.

Upload file và CSRF

Upload file thường là một hành động làm thay đổi dữ liệu trên máy chủ, vì vậy những form quan trọng nên được bảo vệ bằng cơ chế chống CSRF.

Ý tưởng cơ bản là server tạo một token gắn với phiên làm việc của người dùng. Form gửi token lên cùng request và server xác minh token trước khi xử lý upload.

Điều này đặc biệt quan trọng đối với các trang quản trị, nơi người dùng có quyền upload tài liệu hoặc hình ảnh vào hệ thống.

Kiểm tra quyền trước khi upload

Không phải tài khoản nào đăng nhập cũng nên có quyền upload mọi loại file. Một hệ thống quản lý nội dung có thể phân quyền theo vai trò.

Ví dụ:

  • Khách chưa đăng nhập: không được upload.
  • Thành viên: chỉ được upload ảnh đại diện.
  • Tác giả: được upload ảnh cho bài viết.
  • Biên tập viên: được upload thêm tài liệu.
  • Quản trị viên: có quyền quản lý toàn bộ file.

Việc kiểm tra quyền phải diễn ra ở phía server. Không nên chỉ ẩn nút upload bằng HTML hoặc JavaScript rồi coi đó là biện pháp phân quyền, bởi người dùng vẫn có thể tự gửi request trực tiếp đến endpoint.

Upload file bằng AJAX hoặc Fetch API

PHP không bắt buộc phải nhận file từ một form truyền thống. Giao diện hiện đại có thể gửi file bằng JavaScript thông qua FormData.

Ví dụ:

<form id="uploadForm" enctype="multipart/form-data">
    <input type="file" name="document" id="document">
    <button type="submit">Upload</button>
</form>

<script>
document.getElementById('uploadForm').addEventListener('submit', async function (event) {
    event.preventDefault();

    const formData = new FormData(this);

    const response = await fetch('upload.php', {
        method: 'POST',
        body: formData
    });

    const result = await response.text();

    console.log(result);
});
</script>

Phía PHP vẫn xử lý file thông qua $_FILES. Điểm khác biệt nằm ở cách trình duyệt gửi request và cách giao diện nhận kết quả.

Đối với hệ thống hiện đại, endpoint upload có thể trả JSON thay vì HTML. Khi đó JavaScript có thể cập nhật giao diện mà không cần tải lại toàn bộ trang.

Upload file và hiển thị tiến trình

Với file lớn, người dùng thường muốn biết quá trình upload đang diễn ra đến đâu. Giao diện có thể hiển thị phần trăm tiến trình, trạng thái đang tải hoặc thông báo hoàn tất.

Tuy nhiên, thanh tiến trình ở phía trình duyệt không thay thế cho việc kiểm tra phía server. Server vẫn phải xác minh kích thước, loại file, quyền truy cập và trạng thái upload.

Đối với các file rất lớn, hệ thống chuyên nghiệp có thể sử dụng cơ chế upload theo từng phần. Khi đó file được chia thành nhiều chunk nhỏ, gửi lần lượt lên server rồi ghép lại. Đây là một kiến trúc khác với upload PHP truyền thống và cần thiết kế riêng về việc định danh phiên upload, thứ tự chunk, xử lý lỗi và khôi phục khi mất kết nối.

Thay đổi giới hạn upload trong PHP

Khi website cần nhận file lớn hơn giới hạn mặc định, bạn có thể cần điều chỉnh cấu hình PHP. Một số thiết lập thường liên quan đến quá trình này gồm:

  • upload_max_filesize: giới hạn kích thước một file upload.
  • post_max_size: giới hạn tổng dữ liệu của POST request.
  • max_file_uploads: giới hạn số lượng file được upload trong một request.
  • max_execution_time: giới hạn thời gian thực thi script trong một số môi trường.
  • max_input_time: liên quan đến thời gian nhận dữ liệu đầu vào.

Ví dụ cấu hình PHP có thể được viết như sau:

upload_max_filesize = 10M
post_max_size = 20M
max_file_uploads = 10

Các giá trị này không nên được thay đổi tùy tiện. Nếu website chỉ nhận ảnh vài MB, việc đặt giới hạn hàng trăm MB có thể tạo ra rủi ro không cần thiết.

Upload file lên thư mục theo ngày

Với website có lượng file lớn, việc dồn tất cả vào một thư mục duy nhất có thể khiến việc quản lý trở nên khó khăn. Một giải pháp là phân loại thư mục theo năm và tháng.

Ví dụ, file upload trong tháng 7 năm 2026 có thể được lưu vào thư mục tương ứng với năm và tháng. Khi xây dựng đường dẫn động, cần đảm bảo giá trị thư mục được tạo từ dữ liệu đã được kiểm soát, không lấy trực tiếp từ dữ liệu người dùng.

Cách tổ chức này giúp việc backup, di chuyển hoặc dọn dẹp dữ liệu thuận tiện hơn. Tuy nhiên, nếu website có quy mô rất lớn, việc phân vùng theo hash hoặc sử dụng object storage có thể phù hợp hơn.

Lưu trữ file trên dịch vụ bên ngoài

Khi số lượng file tăng cao, máy chủ web không nhất thiết phải là nơi lưu trữ duy nhất. Website có thể sử dụng dịch vụ object storage hoặc CDN để lưu và phân phối file.

Kiến trúc khi đó có thể là:

  1. Người dùng gửi file đến ứng dụng.
  2. PHP kiểm tra và xác thực file.
  3. Ứng dụng gửi file đến hệ thống lưu trữ.
  4. Hệ thống lưu trữ trả về định danh hoặc URL.
  5. PHP lưu thông tin tham chiếu vào cơ sở dữ liệu.

Cách làm này giúp giảm áp lực lên ổ đĩa máy chủ web và phù hợp với các website có lượng dữ liệu lớn. Tuy nhiên, việc triển khai sẽ phức tạp hơn vì cần quản lý quyền truy cập, khóa API, URL riêng tư, thời hạn URL và chính sách xóa file.

Những lỗi thường gặp khi viết chức năng upload

Trong quá trình xây dựng chức năng upload, một số lỗi có thể xuất hiện khá thường xuyên.

Quên enctype multipart/form-data

Đây là lỗi phổ biến nhất ở phía form. Nếu form không có đúng thuộc tính enctype, PHP có thể không nhận được file trong $_FILES.

Chỉ kiểm tra phần mở rộng

Phần mở rộng có thể bị thay đổi dễ dàng. Đối với các file quan trọng, cần xác minh thêm nội dung hoặc MIME type.

Dùng tên file gốc làm tên lưu trữ

Cách này dễ gây trùng tên, khó kiểm soát và có thể tạo ra những vấn đề liên quan đến đường dẫn. Nên tạo tên file mới do hệ thống kiểm soát.

Không kiểm tra kích thước

File không giới hạn kích thước có thể làm tiêu tốn tài nguyên máy chủ. Cần có giới hạn phù hợp với nghiệp vụ.

Không kiểm tra quyền người dùng

Ẩn nút upload trên giao diện không phải là biện pháp bảo mật. Server phải tự kiểm tra quyền của người thực hiện request.

Cho phép file thực thi được truy cập trực tiếp

Đây là một rủi ro nghiêm trọng. Khu vực lưu trữ file người dùng cần được thiết kế để hạn chế khả năng file upload bị máy chủ thực thi như mã nguồn.

Không xử lý lỗi move_uploaded_file()

Upload thành công về mặt request không có nghĩa là file chắc chắn đã được lưu. Hàm di chuyển file có thể thất bại vì quyền thư mục, đường dẫn sai hoặc lỗi hệ thống. Kết quả trả về cần được kiểm tra.

Quy trình upload nên áp dụng cho website thực tế

Nếu cần xây dựng một chức năng upload có thể sử dụng lâu dài, bạn có thể áp dụng quy trình sau:

  1. Xác thực request có đúng phương thức hay không.
  2. Kiểm tra người dùng đã đăng nhập nếu chức năng yêu cầu.
  3. Kiểm tra quyền upload.
  4. Kiểm tra CSRF token đối với form cần bảo vệ.
  5. Kiểm tra có file được gửi lên hay không.
  6. Kiểm tra mã lỗi upload.
  7. Kiểm tra kích thước file.
  8. Kiểm tra phần mở rộng theo danh sách cho phép.
  9. Xác minh MIME type bằng công cụ phù hợp.
  10. Đối với ảnh, kiểm tra thêm cấu trúc hình ảnh.
  11. Tạo tên file mới bằng dữ liệu ngẫu nhiên.
  12. Xác định thư mục lưu trữ an toàn.
  13. Di chuyển file bằng move_uploaded_file().
  14. Lưu metadata vào cơ sở dữ liệu nếu cần.
  15. Ghi log khi có lỗi quan trọng.
  16. Trả kết quả rõ ràng cho giao diện.

Điểm quan trọng nhất là không nên xem upload file như một thao tác đơn giản gồm hai bước "chọn file" và "lưu file". Một hệ thống upload tốt phải đồng thời giải quyết ba bài toán: tính đúng đắn, khả năng quản lýan toàn.

Nên chọn cách lưu file nào?

Phương án Phù hợp với Ưu điểm Điểm cần lưu ý
Lưu trực tiếp trên server Website nhỏ và vừa Dễ triển khai, dễ hiểu Cần quản lý dung lượng và backup
Lưu ngoài thư mục public Tài liệu riêng tư Kiểm soát quyền truy cập tốt hơn Cần endpoint để phục vụ file
Lưu object storage Website nhiều dữ liệu Dễ mở rộng và giảm tải server Cần tích hợp API và quản lý quyền
Lưu kết hợp CDN Website có nhiều ảnh hoặc tài nguyên tĩnh Tăng tốc phân phối nội dung Cần thiết kế cache và invalidation

Không có phương án duy nhất phù hợp với mọi website. Một blog cá nhân có thể chỉ cần thư mục upload trên server. Một hệ thống quản lý tài liệu nội bộ lại cần kiểm soát quyền truy cập chặt chẽ. Trong khi đó, một nền tảng có hàng triệu ảnh có thể cần object storage kết hợp CDN.

Checklist khi xây dựng chức năng upload

Trước khi đưa chức năng upload lên môi trường thật, bạn có thể kiểm tra nhanh theo danh sách sau:

  • Form đã sử dụng multipart/form-data chưa?
  • Server có kiểm tra phương thức request không?
  • Đã xử lý trường hợp không chọn file chưa?
  • Đã kiểm tra mã lỗi upload chưa?
  • Đã giới hạn kích thước file chưa?
  • Đã giới hạn số lượng file chưa?
  • Đã xây dựng danh sách phần mở rộng được phép chưa?
  • Đã xác minh MIME type chưa?
  • File ảnh có được kiểm tra thực tế là ảnh hay không?
  • Tên file lưu trữ có được tạo mới không?
  • Có tránh sử dụng trực tiếp tên file do người dùng gửi làm đường dẫn không?
  • Thư mục upload có quyền phù hợp không?
  • File upload có bị máy chủ thực thi ngoài ý muốn không?
  • Đã kiểm tra quyền người dùng chưa?
  • Form quan trọng đã có CSRF protection chưa?
  • Có lưu metadata nếu cần quản lý file không?
  • Có cơ chế backup và xóa file không còn sử dụng không?
  • Có log lỗi để dễ xử lý sự cố không?

Kết luận

Upload file trong PHP bắt đầu từ một form HTML đơn giản nhưng để xây dựng thành một chức năng đáng tin cậy thì cần nhiều hơn việc gọi move_uploaded_file(). Người lập trình cần hiểu cách $_FILES hoạt động, kiểm tra lỗi, giới hạn dung lượng, xác minh định dạng, tạo tên file an toàn và lựa chọn vị trí lưu trữ phù hợp.

Với các website nhỏ, một quy trình upload có kiểm tra lỗi, MIME type, kích thước và tên file mới đã có thể đáp ứng phần lớn nhu cầu. Khi hệ thống phát triển, bạn nên tiếp tục bổ sung phân quyền, CSRF, lưu metadata, kiểm soát truy cập file, logging, backup và thậm chí chuyển sang object storage nếu dung lượng tăng nhanh.

Điều quan trọng nhất khi làm việc với upload file là luôn coi dữ liệu đến từ người dùng là dữ liệu chưa được tin cậy. Chỉ khi file vượt qua các bước kiểm tra cần thiết, hệ thống mới nên đưa nó vào khu vực lưu trữ chính thức. Tư duy này giúp chức năng upload không chỉ chạy được, mà còn có thể vận hành ổn định và an toàn trong môi trường thực tế.

  • 0 Bình luận
Trần Thị Vân | Content Creator Web Mới
Trần Thị Vân
Trần Thị Vân là Content Creator tại Web Mới, phụ trách nghiên cứu, biên soạn nội dung và chia sẻ kiến thức về website, SEO, lập trình cùng các xu hướng công nghệ
Chia sẻ nội dung đánh giá của bạn về Tổng hợp Upload File trong PHP
Email, Điện thoại của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *
Đánh giá của bạn
Tên *
Email
Số điện thoại *
Bình luận, Hỏi đáp
Đăng ký tư vấn thiết kế website
Tìm hiểu 1 năm không bằng lắng nghe 1 câu tư vấn