Nền tảng An toàn Web & Cơ chế Trình duyệt
16/20
Bài 16 / 20~14 phútNhập môn Threat Modeling & OWASP Sơ cấpMiễn phí… lượt xem

XSS Sơ cấp: Giải mã Reflected và Stored XSS

Cơ chế context switching giữa HTML, JavaScript và CSS context; sự khác biệt giữa Reflected vs Stored XSS và nguyên tắc context-aware encoding.

TL;DR: Cross-Site Scripting (XSS) không đơn thuần là việc ai đó chèn được thẻ <script> vào trang web, mà là hệ quả của việc trình duyệt bị đánh lừa chuyển đổi ngữ cảnh phân tích cú pháp (parser context switching). Khi dữ liệu người dùng được chèn trực tiếp vào phản hồi mà không qua mã hóa đúng ngữ cảnh, trình duyệt sẽ coi dữ liệu đó là mã lệnh thực thi. Dù là Reflected XSS (payload phản hồi tức thì qua tham số URL) hay Stored XSS (payload được lưu trữ vĩnh viễn trong cơ sở dữ liệu), giải pháp khắc phục triệt để không phải là viết regex lọc từ khóa mà là áp dụng context-aware output encoding tại điểm render dữ liệu.

1. Trình duyệt giải mã HTML và JavaScript theo cơ chế nào?

Khi trình duyệt nhận được luồng byte phản hồi từ máy chủ (HTML Document), bộ phân tích cú pháp (HTML Parser) sẽ duyệt qua từng ký tự từ trên xuống dưới để xây dựng cây DOM (Document Object Model).

Về bản chất, trình duyệt liên tục chuyển đổi trạng thái máy (State Machine) tùy thuộc vào ký tự nó đọc được:

  • Đang đọc văn bản bình thường (Data State)
  • Gặp ký tự < ➔ chuyển sang trạng thái đọc tên thẻ (Tag Open State)
  • Gặp thẻ <script> ➔ chuyển hoàn toàn sang trạng thái nạp mã thực thi (Script Data State), bàn giao luồng xử lý cho JavaScript Engine (như V8 trên Chrome)

Cơ chế Context Switching trong XSS

Hình 3.2: Dữ liệu người dùng chưa qua xử lý làm gãy ngữ cảnh của HTML Parser, biến chuỗi ký tự thành mã lệnh JavaScript.

Lỗ hổng XSS xuất hiện khi dữ liệu đầu vào (Untrusted Input) chứa các ký tự điều khiển cú pháp (như <, >, ", ') khiến trình duyệt chuyển ngữ cảnh sớm hơn mong muốn của lập trình viên.

2. Reflected XSS và Stored XSS khác nhau ra sao về mặt kỹ thuật?

Trong phân loại mối đe dọa 01-threat-modeling-va-mo-hinh-stride, XSS là sự kết hợp giữa Tampering (can thiệp vào DOM) và Information Disclosure (đánh cắp phiên hoặc dữ liệu). Về mặt kỹ thuật sơ cấp, XSS chia làm hai dạng chính:

1. Reflected XSS (XSS Không Lưu Trữ)

Payload nằm ngay trong HTTP Request (thường là query string hoặc header) và máy chủ phản ánh (reflect) ngay giá trị đó vào mã HTML trả về:

HTTP
GET /search?q=<script>alert('XSS')</script> HTTP/1.1
Host: victim.com

Phản hồi từ máy chủ:

HTML
<p>Kết quả tìm kiếm cho: <script>alert('XSS')</script></p>
  • Đặc điểm: Không lưu lại trong database.
  • Kịch bản tấn công: Kẻ tấn công tạo một đường link độc hại chứa payload đã mã hóa URL (ví dụ https://victim.com/search?q=%3Cscript...) và gửi cho nạn nhân qua email lừa đảo (phishing) hoặc mạng xã hội.

2. Stored XSS (XSS Lưu Trữ / Persistent)

Payload được máy chủ tiếp nhận và ghi vĩnh viễn vào kho lưu trữ dữ liệu (PostgreSQL, MongoDB, Redis):

  1. Kẻ tấn công đăng bài viết hoặc bình luận trên diễn đàn với nội dung chứa mã độc.
  2. Server lưu chuỗi này vào bảng comments trong cơ sở dữ liệu.
  3. Khi bất kỳ người dùng bình thường nào (kể cả quản trị viên) vào xem bài viết, máy chủ kéo dữ liệu từ database ra và in thẳng vào trang HTML.

[!CAUTION] Mức độ nghiêm trọng: Stored XSS có sức tàn phá lớn hơn Reflected XSS rất nhiều. Kẻ tấn công chỉ cần tiêm mã một lần, toàn bộ người dùng ghé thăm trang web đều sẽ bị thực thi mã độc trong phiên làm việc của họ.

Hãy thử kiểm tra phán đoán của bạn về việc chuyển đổi ngữ cảnh:

Dự đoán & Phản xạ (Predict)

Giả sử máy chủ in dữ liệu biến người dùng vào trong khối JavaScript: <script>var username = 'USER_INPUT';</script>. Nếu server chỉ mã hóa các ký tự HTML (<, >, &) thì có ngăn được XSS không?

3. Vì sao các hàm lọc từ khóa (Blacklisting) luôn thất bại?

Rất nhiều lập trình viên mới bắt đầu thường cố gắng chống XSS bằng cách viết các hàm thay thế regex ngây thơ:

JavaScript
// Giai phap sai lam: Dung Blacklist regex de xoa the script
function sanitizeInput(userInput) {
  return userInput.replace(/<script>/gi, '');
}

Hacker có thể vượt qua bộ lọc này bằng hàng chục kỹ thuật đơn giản:

  1. Lồng thẻ (Recursion): <scr<script>ipt>alert(1)</script> ➔ Sau khi xóa <script>, chuỗi còn lại trở thành <script>.
  2. Sử dụng các thẻ HTML khác đi kèm Event Handler:
    HTML
    <img src="invalid-image" onerror="alert(document.cookie)">
    <svg onload="alert(1)">
    <body onload="alert(1)">
    
  3. Sử dụng giao thức pseudo-protocol javascript::
    HTML
    <a href="javascript:alert(1)">Nhấp vào đây nhận quà</a>
    

HTML5 có hơn 100 thẻ và hàng trăm sự kiện khác nhau. Việc cố gắng lập danh sách đen (blacklist) những từ bị cấm là một cuộc chiến không cân sức mà người phòng thủ chắc chắn sẽ thua.

4. Context-Aware Output Encoding giải quyết bài toán XSS thế nào?

Nguyên tắc vàng để triệt tiêu XSS là: Dữ liệu phải được mã hóa theo đúng ngữ cảnh nơi nó được hiển thị (Context-Aware Output Encoding).

Ngữ cảnh xuất dữ liệuVí dụ vị tríKý tự nguy hiểmChuẩn mã hóa bắt buộc
HTML Body Context<div>USER_INPUT</div><, >, &, ", 'Chuyển thành HTML Entities (&lt;, &gt;, &amp;, &quot;, &#39;)
HTML Attribute Context<input value="USER_INPUT">", ', dấu cách, =Mã hóa toàn bộ ký tự ngoài bảng chữ-số (non-alphanumeric) thành ký tự ASCII Hex
JavaScript Context<script>var x = USER_INPUT;</script>', ", \, ký tự xuống dòngUnicode Escape (\uXXXX) hoặc sử dụng JSON.stringify an toàn
URL Context<a href="/profile?name=USER_INPUT">Dấu cách, &, #, ?URL Encoding chuẩn (encodeURIComponent)

Trong các framework hiện đại (React, Vue, Svelte, Go html/template), cơ chế render mặc định (ví dụ {userName} trong React JSX) đã tự động thực hiện HTML entity encoding, giúp vô hiệu hóa phần lớn các lỗ hổng XSS thông thường.

Tuy nhiên, nguy hiểm vẫn xảy ra khi lập trình viên chủ động "lách luật" bằng các hàm nguy hiểm như dangerouslySetInnerHTML trong React hoặc v-html trong Vue!

5. Tổng kết: Xây dựng tư duy phòng chống XSS bền vững

  1. Output Encoding là chốt chặn số một: Luôn mã hóa dữ liệu theo đúng ngữ cảnh tại thời điểm in ra HTML.
  2. Phòng thủ chiều sâu với CSP: Kết hợp chính sách 04-content-security-policy-csp-co-ban để chặn đứng việc thực thi payload nếu mã hóa bị sơ hở.
  3. Bảo vệ phiên với Cookie Flags: Luôn bật cờ HttpOnly như đã học ở 03-cookie-security-flags để giảm thiểu thiệt hại mất cắp token.

Bài tiếp theo: 03-classic-sqli-va-co-che-lexer

Tự kiểm tra
0/6 câu đã trả lời
  1. Q1
    Bản chất nguyên nhân gốc rễ (Root Cause) của lỗ hổng Cross-Site Scripting (XSS) là gì?
  2. Q2
    Điểm khác biệt quan trọng nhất giữa Reflected XSS và Stored XSS là gì?
  3. Q3
    Tại sao phương pháp lọc từ khóa theo danh sách đen (Blacklist Filtering - ví dụ xóa chuỗi '<script>') lại thất bại trong việc chống XSS?
  4. Q4
    Nếu dữ liệu người dùng được chèn trực tiếp vào thuộc tính href của thẻ liên kết (<a href='USER_INPUT'>), hacker có thể kích hoạt JavaScript bằng cách nào mà không cần dùng ký tự '<' hay '>'?
  5. Q5
    Tại sao việc chỉ mã hóa ký tự HTML Entity (đổi '<' thành '&lt;') là KHÔNG đủ để chống XSS trong ngữ cảnh JavaScript (JavaScript Context)?
  6. Q6
    Cơ chế bảo vệ XSS mặc định của các framework hiện đại như React hoạt động ra sao?

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Classic SQLi: Vì sao chuỗi biến thành câu lệnh trong Lexer