🗺️ Vì sao code AI sinh cần review: bản đồ 6 rủi ro phổ biến

By Better Wise

Code chạy được chưa chắc đã deploy được

Tôi làm review bảo mật đủ lâu để không còn tin vào câu "chạy được rồi anh ơi". Một repo public có key cloud bị commit nhầm thường chỉ sống vài phút trước khi có bot quét tới, vì bot không nghỉ. Đây là bài đầu tiên của module, nên nhiệm vụ của chúng ta chưa phải là sửa từng lỗi, mà là dựng một tấm bản đồ: nhìn vào code AI vừa sinh là biết phải soi chỗ nào trước.

Sau bài này, bạn sẽ:

  • Giải thích được vì sao code do AI sinh cần vòng review riêng, không chỉ cần "chạy test pass".
  • Kể tên và nhận ra 6 nhóm rủi ro phổ biến trong code AI.
  • Chỉ ra được dấu hiệu của từng nhóm trong một đoạn code ngắn.
  • Biết nhóm nào sẽ được mổ xẻ kỹ ở các bài sau, để không hoang mang khi thấy cả 6 cùng lúc.
  • Có thói quen hỏi "dữ liệu này ai gửi tới, và nó đi tới đâu" trước khi bấm commit.

Vì sao AI viết ra code thiếu an toàn

Không phải vì AI "ác ý". Có 5 lý do rất cụ thể, và mỗi lý do đều để lại dấu vết trong code:

  1. AI học từ code có sẵn trên internet. Một lượng lớn tutorial, câu trả lời forum, repo mẫu dùng cách viết nhanh cho dễ hiểu: nối chuỗi SQL, render HTML bằng chuỗi, cors() mở cho mọi origin. AI tái tạo đúng thứ phổ biến, mà thứ phổ biến thì thường là thứ tối giản.
  2. Mục tiêu của AI là code trả lời được prompt. Prompt kiểu "viết API lấy danh sách đơn hàng" thì AI ưu tiên đoạn code ngắn, đủ chạy demo. Kiểm tra quyền, escape output, validate input là phần "không ai yêu cầu" nên hay bị bỏ.
  3. AI không thấy toàn cảnh hệ thống. Nó không biết ai đăng nhập, dữ liệu nào là dữ liệu người dùng, endpoint này nằm sau auth hay không. Thiếu ngữ cảnh thì dễ bỏ qua trust boundary.
  4. Không có vòng phản hồi. AI không bị ai tấn công, không phải trực chiến lúc 2 giờ sáng khi server bị quét. Người review là người duy nhất trong quy trình phải nghĩ tới hậu quả.
  5. Giọng điệu tự tin. Code AI sinh thường gọn gàng, có comment đàng hoàng, trông "xịn". Cảm giác an tâm đó chính là rủi ro: nó làm người review đọc nhanh hơn mức cần thiết.

Note: Không phải đoạn code AI nào cũng có lỗi. Nhưng xác suất có lỗi ở phần "dữ liệu người dùng đi vào hệ thống" cao tới mức mặc định phải soi trước, thay vì tin rồi kiểm tra sau.

Bản đồ 6 rủi ro trong code AI sinh

# Nhóm rủi ro Dấu hiệu AI hay để lại Hậu quả nếu bỏ qua
1 SQL injection Nối chuỗi truy vấn với req.query, req.body Đọc, sửa, xoá dữ liệu ngoài quyền hạn
2 XSS Ghép dữ liệu người dùng vào HTML, dùng innerHTML Chạy JavaScript lạ trong trình duyệt người khác
3 CSRF Endpoint thay đổi dữ liệu mà không có token, chỉ dựa vào cookie Kẻ tấn công mượn session của người dùng
4 Lộ secrets API key, mật khẩu DB viết thẳng trong code hoặc .env bị commit Người ngoài dùng dịch vụ bằng tiền của bạn
5 Thiếu validate và phân quyền ở server Tin vào validate ở client, đổi id trên URL là xem được dữ liệu người khác Rò rỉ dữ liệu, leo thang quyền đơn giản
6 Cấu hình yếu http://, cors({ origin: "*", credentials: true }), bật debug ở production Cookie bị đọc trên đường truyền, site lạ đọc được dữ liệu người dùng

Sáu nhóm này không ngang hàng về tần suất, nhưng cùng một gốc: dữ liệu người dùng đi vào hệ thống mà không ai kiểm tra lại đường đi của nó. Điền thử mấy chỗ trống dưới đây để nhớ gốc đó:

Khi AI viết truy vấn bằng cách _____ với dữ liệu người dùng, dữ liệu đó có thể trở thành một phần của câu lệnh và gây ra _____. Ghép dữ liệu người dùng vào HTML mà không escape gây ra lỗi _____. Thao tác thay đổi dữ liệu thiếu _____ là dấu hiệu của lỗ hổng CSRF. API key không được nằm trong code hay git, mà phải đặt trong _____. Validate input phải làm ở _____, vì kiểm tra ở client có thể bị _____.

Ba đoạn code AI hay viết: gắn nhãn rủi ro

Đọc từng đoạn, gắn nhãn rủi ro theo bảng trên, rồi mở phần đáp án để so.

Đoạn A:

app.get('/orders', async (req, res) => {
  const rows = await db.query(
    "SELECT * FROM orders WHERE user_id = " + req.query.user_id
  );
  res.json(rows);
});

Đoạn B:

const STRIPE_KEY = "sk_live_51H8xQ...";
const DB_URL = "postgres://admin:[email protected]:5432/shop";

Đoạn C:

app.use(cors({ origin: "*", credentials: true }));

app.get('/admin/users', (req, res) => res.json(allUsers));
app.get('/hello', (req, res) => res.send('<h2>Chào ' + req.query.name + '</h2>'));

Xem đáp án và lý do

Đoạn A: SQL injection (nối chuỗi trực tiếp) cộng thêm lỗi phân quyền: đổi user_id là xem được đơn của người khác. Một đoạn 5 dòng, hai rủi ro.

Đoạn B: lộ secrets. Xoá dòng này rồi commit lại cũng không cứu được, vì key đã nằm trong git history. Key phải bị thu hồi và tạo lại, đây là bài học của bài 2.1.

Đoạn C: ba nhóm cùng lúc. CORS mở cho mọi origin kèm credentials: true (nhóm 6), endpoint /admin/users không kiểm tra role (nhóm 5), và ghép req.query.name vào HTML (nhóm 2).

Cách tôi đọc code AI: ba câu hỏi trước khi bấm merge

Ba câu hỏi này rẻ hơn nhiều so với một sự cố, và tôi hỏi chúng theo đúng thứ tự:

  1. Dữ liệu này ai gửi tới? Nếu nó đến từ req.query, req.body, header hay URL, thì mọi chỗ nó xuất hiện đều phải được kiểm tra: trong câu SQL, trong HTML, trong lệnh gọi hệ thống.
  2. Ai được phép gọi endpoint này? Thiếu bước kiểm tra quyền là lỗi tôi gặp nhiều nhất ở code AI, vì AI không biết hệ thống của bạn có role gì.
  3. Có thứ gì bí mật trong đoạn diff không? Key, token, mật khẩu, connection string. Quét bằng mắt từng dòng thêm mới, đừng tin vào cảm giác "chắc không sao".

Vài lỗi thường gặp của người mới:

  • Review bằng cách đọc phần logic, bỏ qua phần cấu hình ở đầu file. Rủi ro số 6 nằm đúng ở đó.
  • Nghĩ rằng "app nội bộ nên không ai tấn công". Bot không đọc được ý định của bạn, nó chỉ quét cổng mở.
  • Sửa lỗi bằng cách thêm validate ở client rồi yên tâm. Client là máy của người dùng, họ tắt JavaScript được.
  • Copy đoạn code AI sửa lỗi mà không đọc lại. AI sửa SQL injection bằng parameterized query rất tốt, nhưng đôi khi nó bỏ luôn kiểm tra quyền đang có.

Điều đáng nhớ trước khi mở bài SQL injection

Sáu nhóm rủi ro, một gốc chung: dữ liệu người dùng đi qua hệ thống mà không có ai kiểm tra lại. AI viết code nhanh và gọn, nhưng nó không chịu trách nhiệm khi dữ liệu đó chạm vào cơ sở dữ liệu, vào trình duyệt người khác, hay khi một key bị commit nhầm. Vai review là của bạn, và nó bắt đầu bằng ba câu hỏi ở trên.

Bài tiếp theo chúng ta mổ xẻ nhóm số 1: vì sao AI rất hay nối chuỗi SQL, một câu injection trông như thế nào khi nhìn từ phía dữ liệu, và cách sửa bằng parameterized query.

Tự kiểm tra bằng thẻ ghi nhớ:

Front: Vì sao code do AI sinh cần được review riêng? Back: AI tối ưu cho đoạn code chạy được theo prompt, thiếu ngữ cảnh hệ thống và không chịu trách nhiệm hậu quả.

Front: Rủi ro SQL injection trong code AI thường bắt nguồn từ đâu? Back: Nối chuỗi truy vấn với dữ liệu người dùng thay vì dùng parameterized query.

Front: XSS xảy ra khi nào? Back: Khi dữ liệu người dùng được render vào HTML mà không escape hoặc encode.

Front: Dấu hiệu của lỗ hổng CSRF là gì? Back: Endpoint thay đổi dữ liệu chỉ dựa vào cookie mà không kiểm tra CSRF token.

Front: Vì sao hardcode API key trong code là sai? Back: Key đi vào git history, ai có repo cũng đọc được, và xoá file không xoá được lịch sử.

Front: Secret nên được đặt ở đâu? Back: Trong biến môi trường hoặc secret manager của môi trường deploy, không nằm trong repo.

Front: Validate input ở client có đủ không? Back: Không, client có thể bị bỏ qua, nên validate bắt buộc phải làm ở server.

Front: Thiếu kiểm tra phân quyền gây ra lỗi gì? Back: Người dùng chỉ cần đổi id trên URL là xem hoặc sửa được dữ liệu của người khác.

Front: Vì sao dùng HTTP thay vì HTTPS là rủi ro? Back: Dữ liệu và cookie đi dạng rõ, có thể bị đọc hoặc sửa trên đường truyền.

Front: Cấu hình CORS mở cho mọi origin kèm credentials gây hậu quả gì? Back: Site lạ có thể đọc dữ liệu của người dùng đang đăng nhập.

Front: Vì sao AI hay bỏ qua bước kiểm tra quyền? Back: AI không thấy toàn cảnh hệ thống và không biết hệ thống của bạn có role nào.

Front: Ba câu hỏi review nhanh trước khi merge là gì? Back: Dữ liệu này ai gửi tới, ai được phép gọi endpoint này, và có secret nào trong diff không.