Hiểu đúng về XSS trong web app hiện đại
Ngày 25 tháng 3, 2026
XSS (Cross-Site Scripting) xảy ra khi dữ liệu không tin cậy bị trình duyệt diễn dịch như mã thực thi. Khi payload được chạy trong origin của website, attacker có thể đánh cắp dữ liệu phiên, thao túng UI hoặc thực hiện hành động thay người dùng.
Bài viết này tham khảo các nguyên tắc từ tài liệu bảo mật của Vercel và chuyển thành checklist triển khai thực tế cho React/Next.js.
1. Luồng tấn công XSS thường gặp
Một kịch bản điển hình:
- Attacker tìm điểm render input không an toàn.
- Chèn payload độc hại.
- User truy cập trang chứa payload.
- Script chạy trong context website hợp lệ.
- Dữ liệu nhạy cảm bị lộ hoặc hành vi trái phép được kích hoạt.
Điểm nguy hiểm nhất của XSS là payload chạy như code “nội bộ” của ứng dụng.
Đi sâu hơn: rủi ro nằm ở execution context
Khi JavaScript độc hại chạy dưới đúng origin của bạn, browser sẽ tin nó như code hợp lệ của hệ thống. Khi đó attacker có thể:
- đọc dữ liệu hiển thị trên trang (kể cả state nhạy cảm render từ server),
- gọi API nội bộ bằng phiên hiện tại của user,
- kích hoạt hành động ngầm trong UI,
- sửa giao diện để lừa user nhập thông tin.
Vì vậy XSS thường không dừng ở một ô input lỗi, mà lan thành rủi ro account/session.
XSS ATTACK CHAIN
2. Ba nhóm XSS quan trọng
DOM-based XSS
Lỗi nằm ở frontend runtime, thường do dùng các sink nguy hiểm như innerHTML, outerHTML, insertAdjacentHTML, hoặc xử lý URL thiếu kiểm soát.
Pattern phổ biến:
- source:
location.search,location.hash,postMessage, dữ liệu widget bên thứ ba, - sink: API DOM parse HTML hoặc context nhạy cảm,
- kết quả: browser chạy payload attacker kiểm soát.
Reflected XSS
Payload đi theo request (query/param) và bị phản chiếu ngay vào response mà không encode/sanitize đúng ngữ cảnh.
Reflected XSS thường đi kèm social engineering vì payload được gói trong URL và phát tán nhanh qua link.
Stored XSS
Payload được lưu vào database/CMS rồi phát tán cho nhiều người dùng. Đây là nhóm thường có impact cao nhất vì tính persistent.
Stored XSS đặc biệt nguy hiểm nếu payload được hiển thị cho admin/moderator, vì có thể trở thành điểm leo thang đặc quyền.
XSS TYPES MATRIX
DOM XSS
Client-side sink misuse
Reflected XSS
Payload reflected in response
Stored XSS
Persistent payload from storage
3. Pattern dễ lỗi và cách thay thế
Không an toàn
// User content bi dien dich nhu HTML
<div dangerouslySetInnerHTML={{ __html: userContent }} />Mặc định an toàn hơn
// React escape text trong JSX theo mac dinh
<div>{userContent}</div>Nếu bắt buộc phải render HTML rich-text
import DOMPurify from "isomorphic-dompurify";
const sanitized = DOMPurify.sanitize(userContent);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;Nguyên tắc: chỉ dùng raw HTML khi yêu cầu nghiệp vụ thực sự cần.
Đi sâu hơn: sanitize chưa đủ nếu sai context
Một giá trị có thể "an toàn" trong context này nhưng lại nguy hiểm ở context khác:
- HTML text context,
- attribute context,
- URL context,
- script context.
Nhiều lỗi production xảy ra khi team sanitize một lần rồi tái sử dụng dữ liệu ở context khác mà không encode lại phù hợp.
4. Validation: client cho UX, server mới là chốt chặn
Validation phía client chỉ giúp trải nghiệm nhập liệu, không phải biên bảo mật.
Backend luôn phải:
- validate schema/type,
- chuẩn hóa và giới hạn format input,
- sanitize nếu chấp nhận HTML,
- encode output theo đúng context hiển thị.
Mọi write-path đều cần áp dụng, không chỉ form trên UI.
Pipeline backend thực dụng
Với user-generated content, nên tách rõ các bước:
- validate schema/type,
- normalize theo policy (length, allow-list),
- sanitize trước khi lưu,
- enforce quy tắc render ở read-path.
Cách này tránh tình trạng endpoint A an toàn nhưng endpoint B bypass policy.
5. Giảm blast radius nếu XSS vẫn xảy ra
Ngoài việc ngăn XSS, cần giảm tác động khi có lỗ hổng:
- cookie phiên đặt
HttpOnlyvàSecure, - dùng
SameSite=LaxhoặcStrictkhi phù hợp, - tránh lưu secret auth dài hạn trong localStorage,
- xoay vòng session/refresh token.
HttpOnly không chặn XSS, nhưng giảm nguy cơ đọc trực tiếp cookie từ JavaScript.
Lưu ý thêm: dù có HttpOnly, script độc hại vẫn có thể gọi action bằng phiên hiện tại. Vì vậy hardening cookie chỉ là giảm thiệt hại, không thay cho phòng chống XSS gốc.
6. CSP là lớp containment quan trọng
Content Security Policy (CSP) giúp giảm khả năng exploit thành công.
Khuyến nghị:
- hạn chế inline script,
- dùng nonce/hash cho script hợp lệ,
- chỉ allow script source đáng tin,
- theo dõi CSP violation report.
CSP không thay thế sanitize/encode, nhưng là lớp giảm thiệt hại rất hiệu quả.
Lưu ý triển khai CSP trong thực tế
- chạy report-only trước để đo mức ảnh hưởng,
- chuyển sang enforce với nonce/hash,
- quản lý policy cùng ownership của build/runtime,
- coi mỗi script third-party mới là một security review event.
XSS DEFENSE LAYERS
7. Checklist thực chiến cho React/Next.js
- Ưu tiên render text bằng JSX mặc định.
- Sanitize khi render HTML từ user-generated content.
- Hạn chế inline script/inline event handler.
- Validate + sanitize ở backend trước khi lưu.
- Cấu hình cookie
HttpOnly,Secure,SameSitehợp lý. - Áp dụng CSP với nonce/hash và whitelist nguồn script.
- Audit thư viện bên thứ ba có thao tác DOM trực tiếp.
- Viết security test cho các sink nguy hiểm (
innerHTML, URL injection).
8. Kết luận
XSS vẫn là một trong các vector tấn công web phổ biến vì nó khai thác niềm tin vào chính origin của bạn. Cách làm bền vững luôn là nhiều lớp:
- xử lý input nghiêm ngặt,
- render output an toàn,
- CSP + hardening cookie/session,
- secure default ở component architecture.
Khi thực hiện nhất quán, XSS sẽ từ rủi ro production thường trực trở thành bài toán có thể kiểm soát.
Mức trưởng thành bảo mật không đến từ một bản vá đơn lẻ, mà từ kỷ luật engineering: secure default, boundary rõ ràng, review liên tục và kiểm tra tự động.