# 🌐 XSS và CSRF: lỗi render dữ liệu và thao tác thiếu token

By Better Wise

## Vì sao bài này quan trọng

Bài trước bạn đã bắt được lỗi AI nối chuỗi SQL và sửa bằng parameterized query. Bài này là người anh em cùng họ của lỗi đó: dữ liệu người dùng vẫn đi vào chỗ không nên đi, nhưng lần này nó đi vào **HTML** và vào **các thao tác làm thay đổi dữ liệu**.

Sự cố tôi hay kể: một bạn dev nhờ AI viết trang profile, AI trả về đúng 3 dòng, trong đó có `el.innerHTML = user.bio`. Sáu tháng sau, một tài khoản đặt bio thành thẻ `img` có `onerror`, và mọi người xem profile đó đều chạy đoạn script lạ trong phiên đăng nhập của chính họ. Không ai review dòng đó, vì nó "chỉ là hiển thị text".

Sau 25 phút này bạn sẽ:

- Nhận ra 2 kiểu render dữ liệu không escape mà AI rất hay sinh ra.
- Viết được bản sửa tối thiểu: `textContent`, escape, hoặc để template engine tự escape.
- Nhận ra endpoint thay đổi dữ liệu thiếu CSRF token và biết 3 lớp phòng thủ đi kèm.
- Phân biệt XSS và CSRF khi ngồi review code người khác.

## XSS: khi AI tin dữ liệu người dùng là văn bản vô hại

XSS xảy ra khi dữ liệu người dùng được đưa vào trang mà không escape, nên trình duyệt hiểu nó là HTML hoặc JavaScript và chạy nó. AI rất hay mắc lỗi này vì `innerHTML` và nối chuỗi là cách ngắn nhất để "làm cho nó chạy".

### Ví dụ 1: `innerHTML` ở frontend

```js
// SAI: AI sinh ra để hiển thị bình luận
const el = document.getElementById('comment');
el.innerHTML = comment.text;
```

Với `comment.text` là `Xin chào <img src=x onerror="...">`, trình duyệt sẽ tạo thẻ `img`, ảnh lỗi, và chạy `onerror`. Đây là **stored XSS**: payload nằm trong database nên ai xem cũng dính.

Bản sửa tối thiểu, khi bạn chỉ cần hiển thị văn bản:

```js
// ĐÚNG: trình duyệt coi toàn bộ là text, không parse thẻ
el.textContent = comment.text;
```

### Ví dụ 2: nối chuỗi ở server

```js
// SAI: reflected XSS
app.get('/hello', (req, res) => {
  res.send('<h1>Xin chào ' + req.query.name + '</h1>');
});
```

Ai gửi link `/hello?name=<script>...</script>` cho nạn nhân là chạy được script trong origin của bạn. Bản sửa tối thiểu: để template engine escape.

```js
// ĐÚNG
res.render('hello', { name: req.query.name });
// hello.ejs: <h1>Xin chào <%= name %></h1>
```

Ghi nhớ cặp cú pháp này của EJS: dấu `=` sau `%` thì **tự escape**, dấu `-` sau `%` thì **in thô**. AI rất thích dùng dấu `-` vì "cho đẹp", và đó chính là lỗ hổng.

Với React, JSX tự escape, nhưng `dangerouslySetInnerHTML` thì tắt escape. Tên prop đã tự cảnh báo, đừng bỏ qua nó.

Lớp phòng thủ bổ sung (không thay thế việc escape): đặt cookie session với `HttpOnly` để script không đọc được cookie, và dùng `Content-Security-Policy` để chặn inline script.

## CSRF: khi trình duyệt tự gửi cookie thay người dùng

CSRF khai thác một hành vi mặc định của trình duyệt: mọi request tới domain A đều tự mang cookie của domain A, **kể cả khi request đó do một site khác khởi tạo**. Kẻ tấn công không cần đọc được gì, chỉ cần khiến trình duyệt gửi request.

### Ví dụ 3: endpoint chuyển tiền không token

```diff
- app.post('/api/transfer', (req, res) => {
-   transfer(req.session.userId, req.body.to, req.body.amount);
-   res.json({ ok: true });
- });
+ app.post('/api/transfer', csrfProtection, (req, res) => {
+   if (!sameOrigin(req)) return res.status(403).end();
+   transfer(req.session.userId, req.body.to, req.body.amount);
+   res.json({ ok: true });
+ });
```

Kẻ tấn công chỉ cần đặt một form ẩn trỏ tới `https://your-app.example/api/transfer` trên trang của họ, kèm một dòng script submit. Người dùng đang đăng nhập mở trang đó, trình duyệt gửi cookie session, server thấy session hợp lệ và chuyển tiền. AI viết endpoint này rất nhanh vì nó chỉ nghĩ tới "đã có session là đủ".

Ba lớp phòng thủ, nên có ít nhất lớp 1 và 2:

1. **CSRF token**: server sinh token gắn với session, form hoặc header phải gửi kèm. Site khác không đọc được token nên không gửi đúng được.
2. **Cookie `SameSite`**: đặt `sameSite: 'Lax'` (hoặc `'Strict'`) để cookie không đi kèm request cross-site.

```js
app.use(session({
  cookie: { httpOnly: true, secure: true, sameSite: 'Lax' }
}));
```

3. **Kiểm tra `Origin`** (dự phòng `Referer`) với mọi request `POST`/`PUT`/`PATCH`/`DELETE`.

Quy tắc: mọi endpoint **thay đổi trạng thái** mà xác thực bằng cookie session đều cần CSRF token. Endpoint chỉ đọc dữ liệu thì không cần, và API dùng bearer token trong header cũng không bị kiểu tấn công này.

## Phân biệt nhanh khi ngồi review

| | XSS | CSRF |
| --- | --- | --- |
| Lỗi gốc | render dữ liệu không escape | thao tác thay đổi dữ liệu thiếu token và thiếu kiểm tra nguồn |
| Cái bị lợi dụng | trình duyệt chạy script trong trang của bạn | trình duyệt tự gửi cookie của bạn |
| Hậu quả | chạy JS trong phiên nạn nhân | thực hiện hành động thay nạn nhân |
| Bản sửa tối thiểu | `textContent`, escape, template engine tự escape | CSRF token + `SameSite` + kiểm tra `Origin` |

Tự kiểm tra nhanh:

Cách sửa tối thiểu cho innerHTML khi chỉ cần hiển thị văn bản là dùng _____.
Trong EJS, cú pháp có dấu _____ sau % thì tự escape, còn dấu _____ thì in thô.
Thuộc tính cookie dùng để chặn trình duyệt gửi cookie trong request cross-site là _____.
Ngoài CSRF token, server nên kiểm tra header _____ với các request thay đổi dữ liệu.

Front: XSS là gì?
Back: Lỗi cho phép dữ liệu người dùng được chạy như HTML hoặc JavaScript trong trình duyệt của người khác.

Front: Stored XSS khác reflected XSS ở đâu?
Back: Stored: payload lưu trong database, ai xem cũng dính. Reflected: payload nằm trong request và phản hồi ngay.

Front: API DOM nào AI hay dùng sai khi render comment?
Back: innerHTML. Nếu chỉ cần hiển thị văn bản thì dùng textContent.

Front: Trong EJS, cú pháp nào tự escape?
Back: Cú pháp có dấu = ngay sau %.

Front: Trong EJS, cú pháp nào in thô và tạo lỗ hổng?
Back: Cú pháp có dấu trừ ngay sau %.

Front: React có tự escape khi render chuỗi không?
Back: Có, trừ khi bạn dùng dangerouslySetInnerHTML.

Front: CSRF là gì?
Back: Kẻ tấn công lợi dụng cookie session của nạn nhân để gửi request thay đổi dữ liệu từ một site khác.

Front: Vì sao CSRF hoạt động được?
Back: Trình duyệt tự gửi cookie của domain đích kèm mọi request, kể cả request do site khác khởi tạo.

Front: CSRF token chặn được gì?
Back: Site khác không đọc được token nên không gửi kèm token đúng trong request giả mạo.

Front: Thuộc tính cookie nào giảm CSRF?
Back: SameSite=Lax hoặc Strict, kèm Secure khi chạy HTTPS.

Front: Cookie HttpOnly giúp gì khi bị XSS?
Back: Script không đọc được cookie session, nên thiệt hại giảm dù XSS vẫn xảy ra.

Front: Header nào nên kiểm tra với POST, PUT, PATCH, DELETE?
Back: Origin, dự phòng bằng Referer.

Front: Những endpoint nào cần CSRF token?
Back: Mọi endpoint thay đổi trạng thái mà xác thực bằng cookie session.

Front: Lớp phòng thủ nào chặn inline script như một lớp bổ sung?
Back: Content-Security-Policy (CSP).

## Bốn câu hỏi tôi luôn hỏi khi đọc code AI có render hoặc POST

- Chuỗi này có đi qua `innerHTML`, `dangerouslySetInnerHTML`, `document.write`, hoặc nối chuỗi HTML ở server không? Nếu có, đó là chỗ phải escape hoặc đổi sang `textContent`.
- Template có dùng cú pháp in thô không? Dấu trừ trong EJS, `|safe` trong Jinja, `v-html` trong Vue, tất cả đều là công tắc tắt escape.
- Endpoint này có thay đổi dữ liệu không? Nếu có và xác thực bằng cookie session, thiếu CSRF token là lỗi chặn deploy.
- Cookie session đã có `HttpOnly`, `Secure`, `SameSite` chưa? Ba thuộc tính này là mặc định an toàn, đừng để AI dùng config mặc định của framework.

Lỗi phổ biến nhất của người mới: sửa XSS bằng cách chặn ký tự `<` bằng regex. Đừng làm vậy, chỉ cần escape đúng ngữ cảnh (HTML, attribute, URL là ba ngữ cảnh khác nhau) hoặc dùng thư viện sanitize đã được kiểm chứng.

## Chốt lại

- XSS là lỗi **render**: dữ liệu người dùng vào HTML mà không escape. Sửa bằng `textContent`, escape đúng ngữ cảnh, hoặc để template engine tự escape.
- CSRF là lỗi **thao tác thiếu token**: endpoint thay đổi dữ liệu dựa vào cookie session mà không có CSRF token, không có `SameSite`, không kiểm tra `Origin`.
- Cả hai đều là loại AI sinh ra trong 3 dòng code và trông rất "bình thường", nên chỉ có review mới bắt được.

Bài tiếp theo là checkpoint của module: bạn sẽ đọc một đoạn code AI viết và tự chỉ ra lỗi injection nằm ở đâu, gồm cả SQL injection ở bài trước và hai lỗi hôm nay.

---

## Contents

Part of [🛡️ Bảo mật web cho vibe coder: OWASP, secrets và dữ liệu người dùng](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung.md)

- **🔍 Nhận diện rủi ro trong code do AI sinh**
  - [🗺️ Vì sao code AI sinh cần review: bản đồ 6 rủi ro phổ biến](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/ban-do-rui-ro-code-ai-sinh.md)
  - [🗄️ SQL injection: AI hay nối chuỗi và cách sửa bằng parameterized query](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/sql-injection-parameterized-query-trong-code-ai.md) (sign-in required)
  - [🌐 XSS và CSRF: lỗi render dữ liệu và thao tác thiếu token](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/xss-csrf-loi-render-va-thieu-token.md) — this lesson
  - [✅ Checkpoint: nhận diện injection trong code AI](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/checkpoint-nhan-dien-injection-code-ai.md)
- **🔐 Secrets, cấu hình và quy trình review trước khi deploy**
  - [🔑 Lộ API key trong repo và quản lý biến môi trường](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/lo-api-key-va-quan-ly-bien-moi-truong.md)
  - [🧱 HTTPS, phân quyền cơ bản và validate input ở server](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/https-phan-quyen-validate-input-server.md)
  - [📋 Checklist review code AI trước khi deploy](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/checklist-review-code-ai-truoc-deploy.md) (premium content)
  - [🏁 Checkpoint: secrets, cấu hình và quy trình review](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/checkpoint-secrets-cau-hinh-review.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/bao-mat-web-cho-vibe-coder-owasp-secrets-va-du-lieu-nguoi-dung/cheatsheet.md) (premium content)
