Bảo mật file upload WordPress không chỉ là kiểm tra đuôi file. Một upload flow an toàn cần kiểm soát ai được upload, loại file nào thực sự cần, file được xác minh thế nào, tên/kích thước ra sao, lưu ở đâu, được phục vụ thế nào và có thể bị thực thi hay không. Nếu một lớp bị bỏ qua, MIME allowlist đơn thuần vẫn có thể để lại execution risk hoặc abuse path.
Mục tiêu không phải chặn mọi file “lạ”, mà là xây một pipeline có thể kiểm chứng: authorize → allowlist → validate → normalize/rename → store → isolate → serve → monitor → review.
WordPress kiểm tra file type ở mức nào?
WordPress có get_allowed_mime_types() để lấy danh sách MIME/extension được phép và wp_check_filetype_and_ext() để đối chiếu extension/MIME, đồng thời cố gắng xác định real file type trong những trường hợp hỗ trợ. Điều này hữu ích nhưng không nên được hiểu là “mọi định dạng đều được content-inspection sâu”.
Vì vậy custom upload flow hoặc plugin nhận loại file nhạy cảm vẫn cần server-side validation phù hợp với định dạng và business requirement, thay vì tin vào filename hoặc header Content-Type do client gửi.
Upload Security Control Matrix
| Control | Mục tiêu | PASS | FAIL |
|---|---|---|---|
| Authorization | Chỉ role/user cần thiết được upload | Capability + route permission rõ | Anonymous/role không phù hợp vẫn upload |
| Extension allowlist | Chỉ loại file business cần | Danh sách hẹp, có owner | Mở rộng vì “tiện” hoặc wildcard |
| Type validation | Không tin filename/Content-Type đơn lẻ | Server-side check phù hợp type | Chỉ kiểm chuỗi extension/header client |
| Filename | Giảm path/name ambiguity | Normalize/generated name, giới hạn length | Dùng trực tiếp tên không kiểm soát |
| File size | Giảm abuse/resource impact | Limit phù hợp use case | Không limit hoặc limit quá rộng |
| Storage | Giảm execution/public exposure | Segregated/outside webroot khi phù hợp | Upload trở thành code path không cần thiết |
| Serving | Kiểm access/content behavior | Serve qua static/handler phù hợp | Server thực thi file dữ liệu như code |
| Monitoring | Phát hiện abuse/drift | Audit/upload/file signal có owner | Không biết ai upload gì/khi nào |
1. Khóa business need trước khi mở thêm file type
Trước khi thêm MIME/extension, ghi rõ loại file nào cần, user/role nào dùng, route nào nhận file, file public hay private, retention bao lâu và downstream system nào xử lý. Nếu business chỉ cần JPG/PNG/PDF, không có lý do mở một tập định dạng rộng hơn.
Không bật ALLOW_UNFILTERED_UPLOADS chỉ để xử lý một use case nhỏ. Nếu chỉ cần thêm một định dạng hợp lệ, hãy cấu hình allowlist hẹp và kiểm tra đúng flow đó.
2. Authorization: upload là quyền ghi dữ liệu
Upload tạo file trên hệ thống nên phải gắn với capability/permission rõ. Review role nào thật sự cần media upload, import hoặc custom attachment. Account bị compromise nhưng không có upload capability có blast radius khác account có quyền ghi file rộng.
Kết hợp Least Privilege WordPress để giảm quyền không cần thiết. Khi review custom handler, đừng dùng admin context thay cho authorization; xem vì sao is_admin() không phải kiểm tra quyền.
3. Extension và MIME: dùng allowlist, nhưng đừng dừng ở đó
Extension allowlist giúp loại bỏ nhiều định dạng không cần thiết. Tuy nhiên client-supplied Content-Type có thể bị giả mạo; filename cũng không chứng minh nội dung thực. Với loại file quan trọng, validation nên dựa trên server-side detection/parsing phù hợp với format.
Không tự viết một “magic byte parser” sơ sài cho mọi định dạng. Ưu tiên API/library/framework đã hiểu format, và coi mismatch giữa extension/type/format là finding cần reject hoặc investigate.
4. Filename handling
Tên file nên được normalize hoặc generated theo ứng dụng, giới hạn length/ký tự và không được dùng làm path tùy ý. Người dùng không cần kiểm soát filesystem path thực. Khi file public, mapping qua attachment ID/handler có thể giúp tách tên business-facing khỏi tên lưu trữ.
5. File size và resource abuse
Upload lớn có thể làm cạn disk, memory, PHP worker, antivirus queue hoặc processing pipeline. Limit phải dựa vào business need và downstream processing capacity. Đừng chỉ đặt limit ở frontend; cần enforcement phía server.
Nếu hệ thống xử lý image/PDF/video sau upload, monitor queue/CPU/I/O để phân biệt traffic hợp lệ với abuse.
6. Storage và execution boundary
Nếu kiến trúc cho phép, lưu file upload trên host/storage tách biệt hoặc ngoài webroot giúp giảm execution risk. Nếu bắt buộc lưu trong webroot, cần đảm bảo vùng dữ liệu không trở thành code execution path ngoài ý muốn và access control phù hợp với public/private use case.
Không copy rule Apache/Nginx từ Internet vào production khi chưa biết stack, CDN, container, object storage hoặc handler hiện tại. Execution isolation phải được kiểm chứng bằng behavior thật.
Upload Isolation Test Matrix
| Test | Expected | PASS | FAIL |
|---|---|---|---|
| Allowed extension | Chỉ type business cần | Allowlist hoạt động | Type ngoài scope vẫn upload |
| Type validation | Không tin client header đơn lẻ | Mismatch bị reject/review | Đổi tên/header là bypass |
| Filename | Tên được sanitize/generated | Không tạo path/name bất thường | User điều khiển path hoặc tên nguy hiểm |
| Size | Limit phù hợp | Oversize bị reject sớm | Processing gây exhaustion |
| Execution isolation | File dữ liệu không chạy như code | Serve static/download behavior | Server thực thi file upload |
| Authorization | Chỉ role được phép | Access control đúng | User không phù hợp upload được |
| Private access | Chỉ actor được phép đọc | Authorization khi download | Biết URL là tải được dữ liệu private |
Safe verification: không cần payload nguy hiểm
Không cần upload web shell hoặc payload độc hại lên production để test isolation. Dùng file kiểm thử vô hại đại diện đúng extension/MIME và quan sát:
- Route có enforce authentication/capability không.
- Filename được đổi/sanitize thế nào.
- File được lưu ở đâu và URL/ID nào đại diện.
- Response khi truy cập file là static/download hay execution path.
- File private có authorization khi tải không.
- Oversize/type mismatch có bị reject đúng không.
- Audit/log có ghi actor, thời gian, route và attachment/file ID không.
Uploads directory không nên trở thành code directory
wp-content/uploads thường là vùng dữ liệu runtime. Nếu phát hiện PHP/script executable ở đây, đó là finding cần review, nhưng chưa tự động chứng minh entry point. Ghi path, hash, timestamp, owner và access log rồi correlate.
Dùng workflow tìm file PHP mới tạo/chỉnh sửa để mở rộng điều tra.
Malware scanning và CDR: control bổ sung, không phải verdict
Nếu business nhận tài liệu người dùng tải lên, antivirus/sandbox hoặc Content Disarm & Reconstruction có thể là defense-in-depth phù hợp với loại file. Nhưng scanner PASS không thay authorization, type validation, storage isolation hoặc serving controls. Đồng thời cần review privacy trước khi gửi file nhạy cảm tới dịch vụ quét bên ngoài.
Upload Verification Record mẫu
Worksheet này để điền bằng kết quả kiểm thử thật; không phải trạng thái mặc định của site.
test_id: UPLOAD-YYYY-NNN
route: ...
actor_role: ...
business_file_type: ...
allowed_extension: ...
detected_type: ...
filename_before: ...
filename_after: ...
size_bytes: ...
storage_class: webroot|outside-webroot|object-storage|other
public_or_private: ...
execution_test: PASS|FAIL|UNKNOWN
authorization_test: PASS|FAIL
oversize_test: PASS|FAIL
type_mismatch_test: PASS|FAIL
private_download_test: PASS|FAIL|N-A
monitoring_event_ref: ...
rollback_or_cleanup: ...
evidence_refs: [...]
Monitoring upload abuse
Alert có thể dựa trên upload rate bất thường, extension/type mới, role không thường upload, file lớn, repeated rejection, executable-like file trong data path hoặc file count growth. Correlate với login/account activity và file integrity thay vì chỉ alert từng file riêng lẻ.
Nếu file lạ xuất hiện ngoài upload flow, xem File Integrity.
PASS/FAIL/HOLD cho upload security
- PASS: business need + owner rõ; role/capability đúng; allowlist hẹp; server-side validation phù hợp; size/name/storage/serving được kiểm; execution isolation PASS.
- FAIL: mở unfiltered upload để “cho chạy”, tin filename/Content-Type client, user không phù hợp upload được, file dữ liệu bị thực thi hoặc private file tải được không authorization.
- HOLD: chưa xác định owner/storage/execution behavior hoặc một integration cần format mới nhưng chưa pilot. HOLD không được mở rộng allowlist trước khi validation xong.
Rollout và rollback khi thêm loại file mới
- Snapshot MIME/permission/config hiện tại.
- Thêm đúng một type/use case.
- Pilot trên staging hoặc nhóm user/route hẹp nếu có thể.
- Chạy Upload Isolation Test Matrix.
- Monitor rejection/error/abuse.
- Nếu validation hoặc business flow fail, revert allowlist/config về state trước.
Nguồn kỹ thuật
- WordPress — wp_check_filetype_and_ext()
- WordPress — get_allowed_mime_types()
- OWASP — File Upload Cheat Sheet
Kết luận
File upload WordPress an toàn cần một pipeline hoàn chỉnh: authorize → allowlist → validate → normalize → store → isolate → serve → monitor → verify. MIME/extension chỉ là một lớp. Control quan trọng nhất là giảm quyền, giảm type, giảm execution surface và kiểm chứng behavior thực tế của storage/serving path.
