Menu

PHP htmlspecialchars(): escape HTML và chống XSS

htmlspecialchars($text) biến năm ký tự có ý nghĩa trong HTML (& < > " ') thành entity, nhờ đó dữ liệu người dùng nhập được hiển thị như văn bản thay vì chạy như mã HTML. Tìm hiểu ENT_QUOTES, escape thuộc tính, mã hóa hai lần, htmlentities và strip_tags.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

htmlspecialchars($text) chuyển &, <, >, " và ' thành &amp;, &lt;, &gt;, &quot; và &#039;. Hãy gọi hàm này với mọi dữ liệu người dùng nhập mà bạn in vào trang, khi đó dữ liệu sẽ hiện ra như văn bản thay vì bị đọc như HTML.

Khối code in cùng một bình luận hai lần, một lần nguyên bản và một lần đã escape. Chạy code rồi so sánh hai dòng ở tab Trang, sau đó gõ HTML của riêng bạn vào form, ví dụ <h1>big</h1> hoặc <img src=x>, rồi nhấn Show.

Ở dòng nguyên bản, trình duyệt làm theo các thẻ: chữ in đậm thì được in đậm, và một người truy cập gõ <script> sẽ khiến script của họ chạy trên trình duyệt của mọi người đọc khác. Kiểu tấn công này gọi là cross-site scripting (XSS). Ở dòng đã escape, cùng những ký tự đó đến dưới dạng &lt;b&gt; và trình duyệt vẽ chúng ra như văn bản. Chuyển sang tab Đầu ra để xem các entity mà PHP thực sự in ra.

htmlspecialchars chuyển đổi những gì

Năm ký tự, không hơn. Chữ cái, dấu và emoji đi qua mà không thay đổi.

& có trong danh sách vì mọi entity đều bắt đầu bằng nó: nếu để nguyên, một bình luận nhắc đến &lt; sẽ bị hiển thị thành <.

Escape cả thuộc tính, không chỉ văn bản

Dữ liệu người dùng nằm trong thuộc tính cũng cần escape không kém. Thiếu nó, một dấu nháy trong giá trị sẽ đóng thuộc tính lại và phần còn lại của dữ liệu trở thành các thuộc tính mới. Ở đây "tên" lén chèn vào một thuộc tính style; hãy chạy và nhìn hai ô nhập.

Ở ô không an toàn, trình duyệt thấy value="Ada" theo sau là một thuộc tính style mới, nên ô chuyển sang màu đỏ và chỉ hiện Ada. Kẻ tấn công sẽ viết onfocus="..." ở đó thay cho style, và code của họ sẽ chạy. Ở ô an toàn, mọi " đã thành &quot;, nên toàn bộ chuỗi nằm gọn trong value và hiện ra đúng như khi gõ.

Từ PHP 8.1, flag mặc định là ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, nên nháy đơn cũng được escape và các thuộc tính viết bằng '...' đều an toàn. Code cũ thường tự truyền ENT_QUOTES, và trên PHP 7 trở về trước thì việc này là bắt buộc:

Một hàm hỗ trợ ngắn cho template

Viết htmlspecialchars($x, ENT_QUOTES, 'UTF-8') hàng chục lần trong một template thì rối mắt, nên phần lớn dự án gói nó vào một hàm tên một chữ cái. Các template engine như Twig và Blade tự động làm điều tương tự cho mọi {{ $var }}.

Kiểu ?string và ?? '' rất quan trọng: truyền null vào htmlspecialchars() đã bị deprecated từ PHP 8.1, và PHP của bạn sẽ in một thông báo deprecated cho mỗi người dùng không có phần giới thiệu.

Mã hóa hai lần và htmlspecialchars_decode

Nếu một giá trị bị escape hai lần, người đọc sẽ thấy các entity: & thành &amp; ở lần đầu và &amp;amp; ở lần thứ hai, và trình duyệt hiển thị nó là &amp;. Điều này thường có nghĩa là giá trị đã bị escape khi lưu và lại bị escape khi in ra. Truyền double_encode: false để giữ nguyên các entity có sẵn, và dùng htmlspecialchars_decode() để chuyển ngược lại.

Cách sửa thật sự là lưu văn bản gốc và chỉ escape khi xuất ra. double_encode: false dành cho văn bản vốn đã chứa những entity không do bạn tạo ra, chẳng hạn một feed được nhập vào.

htmlspecialchars, htmlentities hay strip_tags

Ba hàm này rất hay bị nhầm lẫn. Khối code chạy cả ba trên cùng một đầu vào và cho thấy, với từng hàm, PHP in ra gì và trình duyệt hiểu nó thế nào:

  • htmlspecialchars() escape năm ký tự HTML. Hãy dùng nó cho mọi văn bản bạn in vào HTML.
  • htmlentities() còn biến é thành &eacute;. Hàm này từng hữu ích khi các trang chưa dùng UTF-8; ngày nay nó chỉ làm mã nguồn khó đọc hơn.
  • strip_tags() xóa thẻ và giữ lại phần chữ. Hàm này dùng để biến HTML thành văn bản thuần (bản xem trước email, meta description), không phải để bảo mật: hàng cuối cho thấy thẻ <b> được phép vẫn giữ nguyên onclick, còn văn bản đặt trong thuộc tính thì hoàn toàn không bị đụng tới.

Khi nào htmlspecialchars là chưa đủ

htmlspecialchars() là cách escape đúng cho văn bản HTML và các thuộc tính có dấu nháy. Những chỗ khác trong trang có quy tắc riêng:

  • Trong URL, http_build_query() hoặc urlencode() mã hóa giá trị; sau đó htmlspecialchars() làm cho dấu & giữa các tham số thành HTML hợp lệ.
  • Trong JavaScript, json_encode() tạo ra một giá trị JS hợp lệ, và JSON_HEX_TAG biến < và > thành \u003C và \u003E, nên một </script> trong dữ liệu không thể đóng thẻ script.
  • Đừng bao giờ in dữ liệu người dùng vào href mà không kiểm tra scheme: htmlspecialchars('javascript:alert(1)') không thay đổi gì và vẫn chạy khi được bấm. Chỉ chấp nhận URL http và https, như được trình bày ở trang filter_var.

Để xem cách xử lý form kết hợp tất cả những điều này, hãy đọc form trong PHP.

Câu hỏi thường gặp

htmlspecialchars trong PHP làm gì?

Hàm này thay &, <, >, " và ' bằng &amp;, &lt;, &gt;, &quot; và &#039;. Khi đó trình duyệt hiển thị các ký tự này thay vì đọc chúng như HTML, nên <script> được gõ vào form sẽ hiện ra dưới dạng chữ và không bao giờ chạy.

htmlspecialchars và htmlentities khác nhau thế nào?

htmlspecialchars() chỉ chuyển năm ký tự đặc biệt trong HTML. htmlentities() chuyển cả mọi ký tự có entity được đặt tên, nên café trở thành caf&eacute;. Với trang UTF-8, cả hai đều an toàn như nhau, và htmlspecialchars() giữ cho đầu ra dễ đọc, nên đây là lựa chọn thường dùng.

PHP 8 còn cần ENT_QUOTES không?

Không cần vì lý do an toàn: từ PHP 8.1, flag mặc định là ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, nên nháy đơn cũng được escape. Nhiều dự án vẫn truyền rõ ENT_QUOTES, 'UTF-8' để lời gọi chạy giống nhau trên các phiên bản cũ hơn và người đọc nhìn vào là hiểu.

Nên dùng htmlspecialchars khi nhận dữ liệu hay khi xuất ra?

Khi xuất ra. Hãy lưu và kiểm tra giá trị gốc, rồi escape đúng lúc bạn in nó vào HTML. Escape khi nhận dữ liệu sẽ lưu &lt; vào cơ sở dữ liệu, làm sai độ dài và tìm kiếm, rồi dẫn đến escape hai lần kiểu &amp;lt;.

strip_tags có đủ để chống XSS không?

Không. strip_tags() xóa thẻ nhưng tham số thẻ được phép của nó vẫn giữ nguyên thuộc tính của thẻ, nên <b onclick="..."> vẫn sống sót, và hàm này không làm gì với văn bản đặt bên trong một thuộc tính. Hãy dùng htmlspecialchars() khi in dữ liệu người dùng nhập.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu