Lab

Task
Task 1: Introduction
Cross-site scripting (XSS) vẫn là một trong những lỗ hổng phổ biến đe doạ các ứng dụng web cho đến ngày nay. Các cuộc tấn công XSS dựa vào việc chèn (inject) mã độc vào một trang web tưởng như vô hại để chạy trên trình duyệt của người dùng. Nói cách khác, XSS khai thác sự tin tưởng của người dùng vào ứng dụng web dễ tổn thương, từ đó gây ra thiệt hại.
Một trong những lỗ hổng XSS sớm nhất được ghi nhận vào năm 1999 và dẫn đến khuyến cáo CERT CA-2000-02. Trong nhiều thập kỷ qua, nhiều thực hành bảo mật web vững chắc đã trở thành một phần của các framework ứng dụng web hiện đại, mặc định bảo vệ khỏi nhiều lỗ hổng XSS. Ở chiều ngược lại, động cơ và mức độ tinh vi của các cuộc tấn công cũng ngày càng tăng.
Với sự bùng nổ của ứng dụng web, không hề quá lời khi nói rằng mỗi tháng đều có một vài lỗ hổng XSS được phát hiện và bị khai thác. Trong OWASP Top 10, XSS xếp thứ bảy năm 2017. Ở báo cáo năm 2021, XSS được gộp với các kiểu tấn công injection khác và cùng đứng thứ ba.
Phòng này đi sâu vào XSS để giúp bạn phát hiện, khai thác và vá các lỗ hổng XSS.
Yêu cầu tiên quyết
Bạn cần có kiến thức cơ bản về HTTP và cách trang web hoạt động. Nếu chưa có, hãy hoàn thành mô-đun How the Web Works trước.
Nếu bạn chưa quen với XSS, rất khuyến nghị hoàn thành phòng Intro to Cross-site Scripting, vì phòng hiện tại xây dựng trên những kiến thức đó.
Cuối cùng, phòng này sử dụng các đoạn mã (code snippet) bằng PHP, JavaScript, Python và C#. Không bắt buộc phải biết tất cả các ngôn ngữ này; chúng tôi sẽ giải thích các dòng mã cần thiết và tập trung vào nguyên nhân cũng như biện pháp khắc phục lỗ hổng XSS.
Dưới đây là các phòng bạn có thể dùng để bù đắp kiến thức trước khi bắt đầu:
- HTTP in Detail
- How Websites Work
- JavaScript Essentials
- Intro to Cross-site Scripting
- (Tùy chọn) Python Basics
Mục tiêu học tập
Sau khi hoàn thành phòng này, bạn sẽ hiểu sâu hơn về XSS, cụ thể:
- Reflected XSS
- Stored XSS
- DOM-based XSS
- Cách phòng chống XSS
Chúng tôi cung cấp các đoạn mã minh họa trên những framework web khác nhau để hiểu rõ hơn cả khai thác lẫn bảo vệ trước các lỗ hổng XSS. Trọng tâm không phải là code mà là lý do đằng sau lỗ hổng và một số cách khắc phục. Mục tiêu là giúp bạn hiểu vì sao XSS tồn tại và điều gì khiến nó có thể bị khai thác.
Task 2: Terminology and Types
Như đã nêu, XSS là một lỗ hổng cho phép kẻ tấn công chèn các script độc hại vào trang web được người khác xem. Hệ quả là chúng vượt qua Same-Origin Policy (SOP); SOP là cơ chế bảo mật do các trình duyệt hiện đại triển khai để ngăn một script độc hại trên một trang web truy cập dữ liệu nhạy cảm trên một trang khác. SOP xác định origin dựa trên giao thức (protocol), tên máy chủ (hostname) và cổng (port). Vì vậy, một quảng cáo độc hại không thể truy cập dữ liệu hay thao túng trang và chức năng ở một origin khác, chẳng hạn như trang mua sắm online hay trang ngân hàng. XSS lách SOP vì mã độc được thực thi từ chính cùng origin.
JavaScript phục vụ XSS
Kiến thức cơ bản về JavaScript là then chốt để hiểu các khai thác XSS và điều chỉnh chúng cho nhu cầu của bạn. Vì XSS là tấn công phía client diễn ra trên trình duyệt của nạn nhân, chúng ta nên thử nghiệm trên một trình duyệt tương tự với trình duyệt mục tiêu. Lưu ý rằng các trình duyệt khác nhau có thể xử lý một số đoạn mã khác nhau; nói cách khác, một payload có thể chạy trên Google Chrome nhưng không hoạt động trên Mozilla Firefox hay Safari.
Nếu bạn muốn thử nghiệm JavaScript ngay trong trình duyệt, hãy mở Console trong công cụ dành cho nhà phát triển: Web Developer Tools trên Firefox, Developer Tools trên Google Chrome, và Web Inspector trên Safari. Hoặc dùng phím tắt tương ứng:
- Trên Firefox: Ctrl + Shift + K
- Trên Google Chrome: Ctrl + Shift + J
- Trên Safari: Command + Option + J

Hãy điểm qua và thử một số hàm JavaScript cơ bản:
- Alert: Dùng alert() để hiển thị hộp thoại cảnh báo. Thử alert(1) hoặc alert('XSS') (hay alert("XSS")) để hiện số 1 hoặc chữ “XSS”.
- Console log: Dùng console.log() để in giá trị ra Console của trình duyệt. Ví dụ: console.log(1) và console.log("test text").
- Mã hoá/giải mã Base64: btoa("string") mã hoá dữ liệu nhị phân thành chuỗi ASCII Base64—hữu ích để loại bỏ khoảng trắng/ký tự đặc biệt hoặc mã hoá bảng chữ khác. Hàm ngược là atob("base64_string").
Bạn cũng có thể thử hiển thị các giá trị như cookie hiện tại bằng alert(document.cookie).


Các loại XSS
Nhắc lại từ phòng Intro to Cross-site Scripting, có ba loại XSS chính:
- Reflected XSS: Phụ thuộc vào dữ liệu do người dùng kiểm soát được phản hồi lại trên trang. Ví dụ, trang tìm kiếm hiển thị lại từ khoá bạn nhập; kẻ tấn công sẽ cố nhúng script độc vào tham số tìm kiếm đó.
- Stored XSS: Phụ thuộc vào dữ liệu do người dùng gửi và được lưu trong cơ sở dữ liệu. Ví dụ, người dùng viết đánh giá sản phẩm được lưu và hiển thị cho người khác; kẻ tấn công chèn script vào phần đánh giá để script tự động thực thi trên trình duyệt của người xem khác.
- DOM-based XSS: Khai thác lỗ hổng trong Document Object Model (DOM) để thao túng các phần tử của trang ngay trên client, không cần phản hồi từ server hay lưu trong DB. Đây là loại ít gặp nhất trong ba loại.
Trả lời câu hỏi

Task 3: Causes and Implications
Cross-site scripting (XSS) là một lỗ hổng bảo mật web cho phép kẻ tấn công chèn (inject) các script độc hại vào trang web được người khác xem. Kết quả là người dùng không hay biết sẽ chạy script trái phép trong trình duyệt của họ, dù trang web họ truy cập được coi là an toàn. Vì vậy, XSS có thể là mối đe dọa nghiêm trọng vì nó khai thác niềm tin của người dùng vào trang web.
Điều gì khiến XSS khả thi
Có nhiều lý do khiến lỗ hổng XSS vẫn xuất hiện trong các ứng dụng web. Dưới đây là một vài nguyên nhân:
Thiếu kiểm tra và làm sạch đầu vào
Ứng dụng web nhận dữ liệu người dùng (ví dụ qua form) và dùng dữ liệu đó để tạo HTML động. Do đó, script độc hại có thể được chèn như một phần của dữ liệu hợp lệ và sẽ được trình duyệt thực thi nếu không được làm sạch đúng cách.
Thiếu mã hóa đầu ra
Người dùng có thể dùng nhiều ký tự để làm thay đổi cách trình duyệt xử lý/hiển thị trang. Với HTML, cần mã hóa đúng các ký tự như <, >, ", ', và & về dạng HTML tương ứng. Với JavaScript, cần chú ý escape ký tự ', ", và \. Không mã hóa đúng dữ liệu do người dùng cung cấp là nguyên nhân hàng đầu dẫn đến XSS.
Sử dụng header bảo mật chưa đúng
Nhiều security header có thể giúp giảm thiểu rủi ro XSS. Ví dụ, Content Security Policy (CSP) hạn chế XSS bằng cách quy định nguồn tin cậy cho script có thể thực thi. CSP cấu hình sai (quá cho phép) hoặc dùng unsafe-inline, unsafe-eval không phù hợp sẽ khiến kẻ tấn công dễ thực thi payload hơn.
Lỗ hổng từ framework và ngôn ngữ
Một số framework cũ không có cơ chế chống XSS, hoặc tồn tại lỗ hổng XSS chưa vá. Các framework hiện đại thường tự động escape chống XSS theo thiết kế và vá nhanh khi phát hiện lỗ hổng.
Thư viện bên thứ ba
Tích hợp thư viện bên thứ ba vào ứng dụng web có thể đưa XSS vào, ngay cả khi phần lõi của ứng dụng không có lỗ hổng.
Ví dụ: một hacker độc hại viết bình luận trên website mở đầu bằng lời chào, nói đây là bài viết đầu tiên, và kèm một URL trông như đang đánh cắp cookie của người dùng.

Hệ quả của XSS
Có nhiều hệ quả tiềm ẩn từ XSS. Dưới đây là một số ví dụ:
Chiếm đoạt phiên (Session hijacking)
Vì XSS có thể đánh cắp cookie phiên, kẻ tấn công có thể chiếm quyền phiên và mạo danh nạn nhân nếu thành công.
Lừa đảo và đánh cắp thông tin đăng nhập
Khai thác XSS, kẻ tấn công có thể hiện form đăng nhập giả. Gần đây có trường hợp che một phần trang bằng hộp thoại yêu cầu người dùng kết nối ví tiền mã hóa.
Kỹ nghệ xã hội (Social engineering)
Kẻ tấn công có thể tạo pop-up/alert trông hợp lệ ngay trong website tin cậy, dụ người dùng bấm vào liên kết độc hại hoặc truy cập trang độc hại.
Sửa nội dung và deface
Ngoài phishing và social engineering, kẻ tấn công có thể thay đổi nội dung trang vì mục đích khác, ví dụ làm xấu hình ảnh công ty.
Rò rỉ/tuồn dữ liệu (Data exfiltration)
XSS có thể truy cập và tuồn bất kỳ thông tin nào hiển thị trên trình duyệt người dùng, gồm dữ liệu cá nhân và thông tin tài chính.
Cài phần mềm độc hại
Kẻ tấn công tinh vi có thể dùng XSS để phát tán malware, đặc biệt triển khai drive-by download ngay trên website dễ tổn thương.
Trả lời câu hỏi

Task 4: Reflected XSS
Reflected XSS là một dạng lỗ hổng XSS trong đó script độc hại được “phản hồi” (reflect) về trình duyệt của người dùng, thường thông qua một URL hoặc form được kẻ tấn công chế (crafted). Hãy xem xét một truy vấn tìm kiếm chứa <script>alert(document.cookie)</script>; nhiều người dùng sẽ không thấy đáng ngờ ngay cả khi nhìn kỹ. Nếu được một ứng dụng web dễ tổn thương xử lý, đoạn mã đó sẽ thực thi trong ngữ cảnh trình duyệt của người dùng.
Trong ví dụ vô hại này, nó hiển thị cookie bằng một hộp thoại alert. Rõ ràng, kẻ tấn công muốn làm nhiều hơn là chỉ bật một alert. Tuy nhiên, để tấn công khả thi, ta cần một ứng dụng dễ bị tấn công.
Mô tả kịch bản: Kẻ tấn công nhúng script độc hại vào một URL và gửi cho nạn nhân. Nạn nhân bấm vào URL và truy cập trang đích. Hệ quả là link độc hại được thực thi, và điều xấu xảy ra trên máy của họ.

Ứng dụng web dễ tổn thương
Một lỗ hổng reflected XSS đơn giản là khi người dùng tìm một cụm từ và chuỗi tìm kiếm được chèn nguyên xi vào trang kết quả. Tình huống này tạo mục tiêu dễ cho kẻ tấn công khai thác.
Dù việc phát hiện lỗ hổng này không phải lúc nào cũng dễ, cách khắc phục thì khá thẳng thắn. Đầu vào như <script>alert('XSS')</script> cần được làm sạch (sanitize) hoặc mã hoá HTML thành <script>alert('XSS')</script>.
Trong các phần sau, ta có ví dụ mã dễ tổn thương và mã đã sửa ở các ngôn ngữ/framework:
- PHP
- JavaScript (Node.js)
- Python (Flask)
- C# (ASP.NET)
PHP
Mã dễ bị tấn công
Hãy xem đoạn PHP sau và tìm lý do tại sao nó có thể bị reflected XSS:
<?php
$search_query = $_GET['q'];
echo "<p>You searched for: $search_query</p>";
?>
Nếu bạn chưa quen PHP, $_GET là mảng chứa các giá trị từ query string trên URL. $_GET['q'] là tham số q. Ví dụ, với http://shop.thm/search.php?q=table, thì $_GET['q'] có giá trị table.
Như bạn đoán, lỗ hổng đến từ việc hiển thị giá trị tìm kiếm lên trang mà không làm sạch. Do đó, kẻ tấn công có thể thêm script độc vào URL để nó được thực thi. Ví dụ PoC: http://shop.thm/search.php?q=<script>alert(document.cookie)</script>
Nếu trang dễ tổn thương, một alert sẽ hiện cookie của người dùng.
Mã đã sửa
May mắn là sửa rất đơn giản:
<?php
$search_query = $_GET['q'];
$escaped_search_query = htmlspecialchars($search_query);
echo "<p>You searched for: $escaped_search_query</p>";
?>
Hàm PHP htmlspecialchars() chuyển ký tự đặc biệt sang entity HTML. Các ký tự <, >, &, ", ' mặc định được thay thế để ngăn script trong đầu vào thực thi. (Bạn có thể xem tài liệu của hàm này.)
JavaScript (Node.js)
Mã dễ bị tấn công
Đoạn Node.js dưới đây dễ bị reflected XSS. Hãy chỉ ra phần dễ tổn thương và nghĩ cách sửa:
const express = require('express');
const app = express();
app.get('/search', function(req, res) {
var searchTerm = req.query.q;
res.send('You searched for: ' + searchTerm);
});
app.listen(80);
Đoạn code dùng Express. req.query.q trích giá trị q từ query string. Ví dụ, http://shop.thm/search?q=table khiến req.query.q là table. Phản hồi được tạo bằng cách nối chuỗi người dùng cung cấp.
Vì giá trị lấy từ người dùng và chèn thẳng vào HTML mà không làm sạch/escape, rất dễ gắn payload độc. PoC:
http://shop.thm/search?q=<script>alert(document.cookie)</script>
Nếu trang dễ tổn thương, một alert sẽ hiện cookie.
Mã đã sửa
const express = require('express');
const sanitizeHtml = require('sanitize-html');
const app = express();
app.get('/search', function(req, res) {
const searchTerm = req.query.q;
const sanitizedSearchTerm = sanitizeHtml(searchTerm);
res.send('You searched for: ' + sanitizedSearchTerm);
});
app.listen(80);
Cách sửa là dùng sanitizeHtml() từ thư viện sanitize-html để loại bỏ thẻ/phần tử không an toàn (bao gồm cả <script>). (Xem tài liệu của thư viện.)
Một hướng khác là dùng escapeHtml() thay vì sanitizeHtml(). Đúng như tên, hàm này escape các ký tự như <, >, &, ", '. (Tham khảo trang chủ của thư viện tương ứng.)
Python (Flask)
Mã dễ bị tấn công
Xem ứng dụng Flask đơn giản dưới đây. Hãy tìm phần dễ tổn thương:
from flask import Flask, request
app = Flask(__name__)
@app.route("/search")
def home():
query = request.args.get("q")
return f"You searched for: {query}!"
if __name__ == "__main__":
app.run(debug=True)
request.args.get() lấy tham số từ query string. Ví dụ, http://shop.thm/search?q=table khiến request.args.get("q") là table.
Vì giá trị người dùng được chèn trực tiếp vào HTML mà không sanitize/escape, ta có thể thêm payload độc. PoC:
http://shop.thm/search?q=<script>alert(document.cookie)</script>
Nếu trang dễ tổn thương, sẽ có alert hiển thị cookie.
Mã đã sửa
from flask import Flask, request
from html import escape
app = Flask(__name__)
@app.route("/search")
def home():
query = request.args.get("q")
escaped_query = escape(query)
return f"You searched for: {escaped_query}!"
if __name__ == "__main__":
app.run(debug=True)
Thay đổi chính là escape đầu vào bằng escape() từ module html. Lưu ý: trong ngữ cảnh Flask, html.escape() tương đương mục đích với markupsafe.escape() (được dùng trong hệ sinh thái Werkzeug/Jinja) — đều nhằm escape ký tự không an toàn (<, >, ", ') sang entity HTML, vô hiệu hoá mã độc trong chuỗi.
ASP.NET (C#)
Mã dễ bị tấn công
Đoạn code ASP.NET C#:
public void Page_Load(object sender, EventArgs e)
{
var userInput = Request.QueryString["q"];
Response.Write("User Input: " + userInput);
}
Request.QueryString trả về tập key–value từ query string. Ở đây ta lấy giá trị của q vào userInput, rồi ghi thẳng vào phản hồi.
Mã đã sửa
using System.Web;
public void Page_Load(object sender, EventArgs e)
{
var userInput = Request.QueryString["q"];
var encodedInput = HttpUtility.HtmlEncode(userInput);
Response.Write("User Input: " + encodedInput);
}
Một lần nữa, giải pháp là mã hoá đầu vào thành chuỗi an toàn với HTML. ASP.NET C# cung cấp HttpUtility.HtmlEncode(), chuyển các ký tự như <, >, & thành entity HTML tương ứng để ngăn thực thi mã độc.
Trả lời câu hỏi

Task 5: Vulnerable Web Application 1
Trả lời câu hỏi

Task 6: Stored XSS
Stored XSS (còn gọi là Persistent XSS) là một lỗ hổng bảo mật ứng dụng web xảy ra khi ứng dụng lưu dữ liệu do người dùng cung cấp và nhúng lại dữ liệu đó vào các trang web phục vụ người dùng khác mà không được làm sạch (sanitize) hay escape đúng cách. Ví dụ thường gặp gồm: bài viết trên diễn đàn, đánh giá sản phẩm, bình luận của người dùng và các kho dữ liệu khác. Nói cách khác, stored XSS xuất hiện khi đầu vào của người dùng được lưu trong kho dữ liệu và sau đó được chèn vào trang phục vụ cho người dùng khác mà không escape đầy đủ.
Kịch bản: kẻ tấn công đăng một bình luận có chứa script độc hại. Người dùng khác xem bình luận đó. Hệ quả là có hành vi độc hại xảy ra trên máy của họ.

Stored XSS bắt đầu khi kẻ tấn công chèn script độc vào một ô nhập liệu của ứng dụng web dễ tổn thương. Lỗ hổng có thể nằm ở cách ứng dụng xử lý dữ liệu trong ô bình luận, bài viết diễn đàn hoặc mục thông tin hồ sơ. Khi người dùng khác truy cập nội dung đã lưu này, script độc hại được inject sẽ thực thi trong trình duyệt của họ. Script có thể làm rất nhiều việc: từ đánh cắp cookie phiên đến thực hiện hành động thay người dùng mà không có sự đồng ý.
Ứng dụng web dễ tổn thương
Có nhiều nguyên nhân khiến ứng dụng web bị stored XSS. Một số thực hành tốt để ngăn chặn:
- Xác thực và làm sạch đầu vào: Đặt quy tắc rõ ràng và kiểm tra chặt chẽ mọi dữ liệu do người dùng cung cấp. Ví dụ: tên người dùng chỉ cho phép chữ và số; trường tuổi chỉ cho phép số nguyên.
- Escape khi xuất (output escaping): Khi hiển thị dữ liệu người dùng trong ngữ cảnh HTML, hãy mã hoá các ký tự đặc thù như <, >, &.
- Mã hoá theo ngữ cảnh: Trong JavaScript, cần dùng JS-encoding khi đưa dữ liệu vào mã JS. Dữ liệu đặt trong URL phải dùng kỹ thuật URL-encoding phù hợp (như percent-encoding) để URL vẫn hợp lệ và ngăn chèn script.
- Phòng thủ nhiều lớp (defence in depth): Đừng dựa vào một lớp bảo vệ duy nhất; hãy dùng xác thực phía server thay vì chỉ dựa vào xác thực phía client.
Trong các ví dụ sau, ta liệt kê các đoạn mã dễ tổn thương ở nhiều ngôn ngữ. Bài này khá dễ vì cách sửa giống với phần trước.
PHP
Mã dễ bị tấn công
Đoạn mã dưới đây có nhiều lỗ hổng. Nó làm 2 việc:
- Đọc bình luận từ người dùng và lưu vào biến $comment.
- Thêm $comment vào cột comment trong bảng comments của CSDL.
Sau đó, nó duyệt tất cả các dòng trong cột comment và hiển thị ra màn hình.
Hãy xem và nghĩ xem điều gì có thể sai.
// Storing user comment
$comment = $_POST['comment'];
mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");
// Displaying user comment
$result = mysqli_query($conn, "SELECT comment FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
echo $row['comment'];
}
Trong phạm vi bài này ta tập trung vào stored XSS (SQL injection nằm ngoài phạm vi). Vấn đề chính: bình luận được lưu và sau đó hiển thị lại cùng các bình luận khác mà không sanitize.
Mã đã sửa
// Storing user comment
$comment = mysqli_real_escape_string($conn, $_POST['comment']);
mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");
// Displaying user comment
$result = mysqli_query($conn, "SELECT comment FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
$sanitizedComment = htmlspecialchars($row['comment']);
echo $sanitizedComment;
}
Trước khi hiển thị mỗi bình luận, ta gọi htmlspecialchars() để chuyển ký tự đặc biệt thành entity HTML. Vì vậy, mọi cố gắng thực hiện stored XSS sẽ không tới được trình duyệt của người dùng.
(Ngoài phạm vi bài: nếu bạn tò mò về SQLi, có thể giảm thiểu bằng mysqli_real_escape_string(), hàm này escape các ký tự đặc biệt trong chuỗi đầu vào để có thể dùng an toàn trong câu lệnh SQL.)
JavaScript (Node.js)
Mã dễ bị tấn công
Đoạn JS sau đọc bình luận của người dùng đã lưu trong bảng DB (mảng comments giả định đã được lấy từ DB). Hãy tìm xem vì sao nó bị stored XSS và cách khắc phục.
app.get('/comments', (req, res) => {
let html = '<ul>';
for (const comment of comments) {
html += `<li>${comment}</li>`;
}
html += '</ul>';
res.send(html);
});
Vấn đề: giá trị comment (do người dùng nhập và đã lưu) được chèn thẳng vào HTML. Khi người dùng khác xem, trình duyệt sẽ thực thi mọi script được chèn trong đó.
Mã đã sửa
const sanitizeHtml = require('sanitize-html');
app.get('/comments', (req, res) => {
let html = '<ul>';
for (const comment of comments) {
const sanitizedComment = sanitizeHtml(comment);
html += `<li>${sanitizedComment}</li>`;
}
html += '</ul>';
res.send(html);
});
Một phần lời giải là sanitize HTML trước khi hiển thị. Ta có thể loại bỏ các phần tử ngoài allowlist bằng sanitizeHtml(). Thông thường, ta cho phép định dạng chữ cơ bản như đậm/nghiêng (<b>, <i>), còn loại bỏ các phần tử nguy hiểm như <script> hay các thuộc tính sự kiện như onload. (Xem thêm trên trang chính của thư viện.)
Python (Flask)
Mã dễ bị tấn công
Tương tự các ví dụ trước, đoạn mã sau dùng Flask. Đến đây bạn có thể đoán được các lỗi phổ biến.
from flask import Flask, request, render_template_string
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String, nullable=False)
@app.route('/comment', methods=['POST'])
def add_comment():
comment_content = request.form['comment']
comment = Comment(content=comment_content)
db.session.add(comment)
db.session.commit()
return 'Comment added!'
@app.route('/comments')
def show_comments():
comments = Comment.query.all()
return render_template_string(''.join(['<div>' + c.content + '</div>' for c in comments]))
Vấn đề thứ nhất: comment_content được lấy từ request.form['comment'] không sanitize, tạo điều kiện cho stored XSS và SQL injection. Thêm nữa, khi hiển thị, các bình luận được đưa vào HTML mà không escape, lại càng dễ stored XSS.
Mã đã sửa
Ta tập trung sửa stored XSS. Cần đảm bảo không lưu script độc vào DB; đồng thời, escape mọi nội dung trước khi đưa vào HTML.
from flask import Flask, request, render_template_string, escape
from flask_sqlalchemy import SQLAlchemy
from markupsafe import escape
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.String, nullable=False)
@app.route('/comment', methods=['POST'])
def add_comment():
comment_content = request.form['comment']
comment = Comment(content=comment_content)
db.session.add(comment)
db.session.commit()
return 'Comment added!'
@app.route('/comments')
def show_comments():
comments = Comment.query.all()
sanitized_comments = [escape(c.content) for c in comments]
return render_template_string(''.join(['<div>' + comment + '</div>' for comment in sanitized_comments]))
Ta dùng escape() để đảm bảo các ký tự đặc biệt trong bình luận do người dùng nhập được chuyển sang entity HTML. Như bạn mong đợi, các ký tự &, <, >, ', " sẽ trở thành &, <, >, ', ". Có hai thay đổi:
- Dù request.form['comment'] vẫn được lưu nguyên, nhưng khi hiển thị, mỗi c.content đều đi qua escape() trước khi trả về trình duyệt.
C# (ASP.NET)
Mã dễ bị tấn công
Đoạn C# sau có nhiều lỗ hổng. Hãy xem nhanh và nghĩ xem cần thay đổi gì.
public void SaveComment(string userComment)
{
var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES ('" + userComment + "')", connection);
// Execute the command
}
public void DisplayComments()
{
var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader();
while (reader.Read())
{
Response.Write(reader["Comment"].ToString());
}
// Execute the command
}
Một lỗ hổng thấy rõ là stored XSS: hệ thống lưu mọi bình luận người dùng nhập và hiển thị lại cho người khác. (SQL injection cũng tồn tại nhưng nằm ngoài phạm vi bài này.)
Mã đã sửa
using System.Web;
public void SaveComment(string userComment)
{
var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES (@comment)", connection);
command.Parameters.AddWithValue("@comment", userComment);
}
public void DisplayComments()
{
var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader();
while (reader.Read())
{
var comment = reader["Comment"].ToString();
var sanitizedComment = HttpUtility.HtmlEncode(comment);
Response.Write(sanitizedComment);
}
reader.Close();
}
Chỉ với vài thay đổi, độ an toàn đã được cải thiện. Stored XSS được khắc phục bằng cách dùng HttpUtility.HtmlEncode() trước khi hiển thị userComment trong trang web.
(Nếu bạn tò mò: SQL injection được khắc phục bằng truy vấn tham số hoá — truyền giá trị riêng thay vì nối chuỗi SQL. Có thể dùng Parameters.AddWithValue() trong SqlCommand.)
Trả lời câu hỏi

Task 7: Vulnerable Web Application 2
Trả lời câu hỏi

Task 8: DOM-Based XSS
Nếu bạn xem các Security Advisories (khuyến cáo bảo mật) cập nhật, sẽ thấy hàng tháng vẫn xuất hiện các lỗ hổng reflected và stored XSS mới. Tuy nhiên, điều tương tự không đúng với DOM-based XSS, vốn đang ngày càng hiếm. Lý do là DOM-based XSS hoàn toàn diễn ra trên trình duyệt và không cần đi tới server rồi quay lại client. Từng có thời, chỉ với một trang HTML tĩnh cũng có thể tạo PoC cho DOM-based XSS; nhưng nhờ bảo mật tích hợp của trình duyệt được cải thiện, DOM-based XSS nay trở nên rất khó.
Trước khi đi sâu vào DOM-based XSS, hãy ôn lại Document Object Model (DOM) là gì. DOM là giao diện lập trình biểu diễn một tài liệu web như cây. DOM cho phép truy cập và thao tác các phần khác nhau của website bằng JavaScript. Cùng xem một ví dụ thực tế.
Xét mã HTML của example.com trong ảnh chụp dưới (lấy ngày 1/2/2024). Ta mở Web Developer Tools trên Firefox và xem thẻ Inspector.
(Một trình duyệt với tab Inspector hiển thị một website ví dụ.)

Cây DOM hiển thị ở trên tương tự danh sách có các danh sách con sau:
- document
- <!DOCTYPE html>
- html
- head
- title
- meta
- meta
- meta
- style
- body
- div
- h1
- p
- p
- a
- div
- head
Cây bắt đầu với nút document và tách nhánh sang DOCTYPE và html. Nút html tách thành head và body. head chứa title, vài thẻ meta, và style. Trong ví dụ đơn giản này, body có một div, trong đó có một h1 và hai p. Đây là trang rất ngắn gọn; các trang thực tế sẽ có hàng chục hoặc hàng trăm nhánh.
Ta có thể xem cây DOM bằng công cụ nhà phát triển tích hợp của trình duyệt. Ví dụ, bấm Ctrl + Shift + I trên Firefox rồi mở thẻ Inspector.
Ngoài ra, ta có thể mở bảng điều khiển JavaScript (Console) như đã nói ở Nhiệm vụ 2. Dùng JavaScript, bạn có thể thao tác cây DOM: tạo phần tử mới với document.createElement() và thêm con vào phần tử bằng element.append(). Ví dụ từ tài liệu MDN:
let div = document.createElement("div");
let p = document.createElement("p");
div.append(p);
console.log(div.childNodes); // NodeList [ <p> ]
Trong ví dụ trên, ta tạo hai phần tử div và p, sau đó gắn p làm con của div.
Ứng dụng web dễ tổn thương
DOM-based XSS xảy ra ngay trong trình duyệt. Nó không cần gửi dữ liệu tới server rồi trả về trình duyệt của người dùng. Nói cách khác, kẻ tấn công sẽ tìm cách chèn script độc (ví dụ vào URL), và script sẽ thực thi phía client mà server không tham gia. Để minh họa, ta sẽ đưa một site tĩnh cực kỳ tối giản, không dựa vào mã back-end.
Site “tĩnh” dễ tổn thương
Xét ví dụ cơ bản sau. Nó quá đơn giản đến mức thiếu thực tế, nhưng đủ để minh họa DOM-based XSS.
<!DOCTYPE html>
<html>
<head>
<title>Vulnerable Page</title>
</head>
<body>
<div id="greeting"></div>
<script>
const name = new URLSearchParams(window.location.search).get('name');
document.write("Hello, " + name);
</script>
</body>
</html>
Trang trên mong người dùng cung cấp tên sau ?name=. Trong ảnh chụp:
- Người dùng nhập Web Tester sau ?name trên URL.
- Lời chào hoạt động như mong đợi và hiển thị “Hello, Web Tester”.
- Cấu trúc DOM bên phải giữ nguyên; body có ba phần tử con trực tiếp.

(Một trình duyệt với tab Inspector hiển thị cấu trúc DOM ban đầu của site tĩnh ví dụ.)
Người dùng có thể thử chèn script độc. Trong ảnh tiếp theo:
- Người dùng thêm <script>alert("XSS")</script> thay vì chỉ “Web Tester” cho tham số name.
- Script được thực thi, hộp thoại alert xuất hiện.
- Quan trọng nhất: cây DOM thay đổi — body giờ có bốn phần tử con.

(Một trình duyệt với tab Inspector hiển thị cấu trúc DOM đã bị thao túng của site tĩnh ví dụ.)
Ví dụ cơ bản này cho thấy:
- Server không có vai trò trực tiếp trong lỗ hổng DOM-based; tất cả diễn ra trên trình duyệt, không cần back-end.
- DOM bị sửa không an toàn do dùng document.write().
Site “tĩnh” đã sửa
<!DOCTYPE html>
<html>
<head>
<title>Secure Page</title>
</head>
<body>
<div id="greeting"></div>
<script>
const name = new URLSearchParams(window.location.search).get('name');
// Escape the user input to prevent XSS attacks
const escapedName = encodeURIComponent(name);
document.getElementById("greeting").textContent = "Hello, " + escapedName;
</script>
</body>
</html>
Một cách khắc phục là tránh thêm trực tiếp đầu vào của người dùng bằng document.write(). Thay vào đó, ta escape đầu vào với encodeURIComponent() rồi gán vào textContent.
Lúc này, thử nghiệm trước đó không còn hiệu quả. Ta thấy rằng:
- Người dùng có chèn JavaScript trong đầu vào.
- Mã JavaScript được hiển thị dưới dạng đã mã hoá và không gây nguy hiểm trong ngữ cảnh hiện tại.
- Cấu trúc DOM không còn bị ảnh hưởng khi người dùng cố gắng thêm mã vào phần tên đã gửi.

(Một trình duyệt với tab Inspector hiển thị cấu trúc DOM của site đã vá.)
Trả lời câu hỏi

Task 9: Context and Evasion
Ngữ cảnh (Context)
Payload XSS được chèn vào thường sẽ rơi vào một trong các ngữ cảnh sau:
- Giữa các thẻ HTML (between HTML tags)
- Bên trong thẻ HTML (within HTML tags / thuộc tính)
- Bên trong JavaScript (inside JS code)
Khi XSS xảy ra giữa các thẻ HTML, kẻ tấn công có thể chạy: <script>alert(document.cookie)</script
Khi chèn bên trong một thẻ/thuộc tính HTML, ta cần kết thúc thẻ/thuộc tính hiện tại để nhường “lượt” cho script được nạp. Vì vậy, payload có thể điều chỉnh thành: ><script>alert(document.cookie)</script> hoặc "><script>alert(document.cookie)</script> hay biến thể tương tự phù hợp với ngữ cảnh.
Nếu có thể chèn XSS vào trong một đoạn JavaScript sẵn có, đôi khi cần đóng đoạn script hiện tại để chạy đoạn chèn. Ví dụ, bắt đầu bằng: </script> rồi tiếp tục payload của bạn.
Nếu mã của bạn nằm trong một chuỗi JavaScript, có thể đóng chuỗi bằng ký tự ', kết thúc câu lệnh bằng dấu chấm phẩy ;, thực thi lệnh của bạn, rồi comment phần còn lại bằng //. Ví dụ: ';alert(document.cookie)//
Những ví dụ này gợi ý cách thoát khỏi ngữ cảnh ban đầu để thực thi payload. Nói chung, nhận diện đúng ngữ cảnh nơi payload XSS của bạn sẽ chạy là rất quan trọng để tấn công thành công.
Né tránh (Evasion)
Có nhiều kho/payload list để tham khảo khi xây dựng payload XSS tùy biến, cho phép bạn thử nghiệm rộng rãi. Ví dụ: XSS Payload List.
Tuy nhiên, đôi khi có bộ lọc chặn payload XSS:
- Nếu bị giới hạn độ dài, danh sách Tiny XSS Payloads là điểm bắt đầu tốt để vượt hạn chế.
- Nếu payload bị chặn theo blocklist ký tự/mẫu, có nhiều mẹo để né. Ví dụ, tab ngang, xuống dòng, hoặc carriage return có thể chia nhỏ payload để vượt qua cơ chế phát hiện.
Mã hexa của các ký tự điều khiển thường dùng:
- Horizontal Tab (TAB): 0x09
- New line (LF): 0x0A
- Carriage return (CR): 0x0D
Theo XSS Filter Evasion Cheat Sheet, ta có thể “bẻ” payload
thành nhiều biến thể:
<IMG SRC="jav	ascript:alert('XSS');">
<IMG SRC="jav
ascript:alert('XSS');">
<IMG SRC="jav
ascript:alert('XSS');">
Có hàng trăm kỹ thuật né tránh; lựa chọn phụ thuộc vào cơ chế bảo vệ của mục tiêu và thường đòi hỏi thử sai trước khi tìm được biến thể hiệu quả.
Trả lời câu hỏi

Task 10: Conclusion
Mục tiêu của phòng này là giúp bạn hiểu sâu hơn về cơ chế hoạt động nền tảng của các script XSS. Chúng tôi đã trình bày các nguyên nhân và nhiều biện pháp khắc phục cho XSS. Hiểu những gì diễn ra “hậu trường” sẽ cho bạn lợi thế khi khám phá các khai thác hiện có và điều chỉnh chúng phù hợp với nhu cầu của mình.
Trả lời câu hỏi
