Loạt bài phòng thủ: hiểu lỗ hổng để viết code an toàn, demo trong lab cô lập.
Nhiều người nghĩ HTTPS chỉ là "mã hoá dữ liệu". Thực ra nó làm hai việc tách biệt và đều quan trọng: (1) mã hoá — không ai nghe lén được nội dung; và (2) xác thực — đảm bảo bạn đang nói chuyện với đúng server, không phải kẻ giả mạo. Chính phần thứ hai mới chống được tấn công người-đứng-giữa (MITM): nếu không xác thực server, bạn có thể đang mã hoá dữ liệu... và gửi thẳng cho kẻ tấn công.
Và đây là lúc một dòng code tưởng vô hại trở thành thảm hoạ: InsecureSkipVerify: true. Lập trình viên gặp lỗi chứng chỉ (thường khi dev với cert tự ký), tìm trên mạng, thấy "đặt InsecureSkipVerify=true là hết lỗi", copy vào — và vô tình tắt toàn bộ phần xác thực của TLS. Kết nối "chạy", nhưng HTTPS giờ chỉ còn mã hoá mà không biết mã hoá với ai. Bài này (phần 9 loạt Bảo mật web) tái hiện thật cái bẫy đó và cách làm đúng.
Cơ chế: mã hoá cần đi với xác thực

Hình 1: Mặc định verify cert — kiểm cert server do CA tin cậy ký, đúng tên miền, còn hạn → chắc chắn đúng server (chống MITM). InsecureSkipVerify: true — chấp nhận mọi cert: kênh vẫn mã hoá nhưng không biết với ai, mở cửa MITM. Cách đúng khi cần cert riêng: thêm vào RootCAs (verify vẫn bật, chỉ tin cert bạn chủ động thêm).
Tái hiện thật trong go-lab
Mình dựng trong go-lab (golang 1.23) một server TLS với cert tự ký (httptest.NewTLSServer), rồi thử ba kiểu client.

Hình 2: Kết quả thật — ① client mặc định (verify): LỖI x509: certificate signed by unknown authority (từ chối cert tự ký, đúng); ② InsecureSkipVerify=true: THÀNH CÔNG đọc được dữ liệu nhạy cảm (chấp nhận cert không hợp lệ — bẫy); ③ thêm cert vào RootCAs: THÀNH CÔNG với verify vẫn bật (an toàn).
Ba kịch bản cho thấy đâu là đúng, đâu là bẫy:
- Mặc định: từ chối cert không tin được. Client Go mặc định verify đầy đủ: kiểm cert có do một CA (Certificate Authority) tin cậy ký không, có đúng tên miền không, còn hạn không. Với cert tự ký (không CA nào tin), nó trả lỗi rõ ràng
x509: certificate signed by unknown authorityvà từ chối kết nối. Đây là hành vi đúng — nó bảo vệ bạn. - InsecureSkipVerify=true: kết nối được, nhưng mù. Đặt cờ này, client bỏ qua mọi kiểm tra và kết nối thành công, đọc được "dữ liệu nhạy cảm". Lỗi biến mất — và đó chính xác là lý do nó nguy hiểm: trông như đã sửa xong, nhưng thực ra client giờ chấp nhận bất kỳ cert nào. Kẻ tấn công đứng giữa (trên WiFi công cộng, router bị chiếm, DNS giả) có thể trình một cert giả, client vui vẻ chấp nhận, và kẻ đó giải mã toàn bộ lưu lượng — đọc mật khẩu, token, dữ liệu — mà không gì cảnh báo. Kênh vẫn mã hoá, nhưng mã hoá với kẻ tấn công.
- Thêm cert vào RootCAs: cách đúng. Khi bạn thực sự cần tin một cert riêng (cert tự ký nội bộ, internal CA), đừng tắt verify — hãy thêm cert đó vào trust pool (
RootCAs). Verify vẫn bật, chỉ là giờ nó cũng tin cert bạn chủ động thêm. Kết nối thành công và an toàn: client vẫn từ chối mọi cert khác, chỉ chấp nhận đúng cert bạn tin. Đây là khác biệt cốt lõi: "tin thêm một cert cụ thể" an toàn, "tin mọi cert" là tự sát.
Đánh đổi cần cân nhắc
KHÔNG BAO GIỜ InsecureSkipVerify trong production — và nó hay bị quên. Vấn đề lớn nhất không phải kỹ thuật mà là quy trình: lập trình viên đặt InsecureSkipVerify=true để "cho chạy nhanh" lúc dev với cert tự ký, định sửa sau, rồi quên, và nó lọt lên production. Vì kết nối vẫn hoạt động bình thường (không lỗi, không cảnh báo), không ai phát hiện cho tới khi bị tấn công. Quy tắc: đừng bao giờ dùng nó như cách sửa lỗi cert; nếu dev cần cert tự ký, thêm vào trust pool đúng cách ngay từ đầu. Dùng linter/security scanner để bắt InsecureSkipVerify=true trong code.
Dùng TLS phiên bản hiện đại và cấu hình tốt. Ngoài verify cert, chất lượng TLS còn ở phiên bản và cipher: ép TLS 1.2 trở lên (TLS 1.0/1.1 đã lỗi thời, có lỗ hổng), tắt các cipher yếu. Về phía server web, bật HSTS (Strict-Transport-Security header) để trình duyệt ép dùng HTTPS cho mọi kết nối tới domain (chống tấn công hạ cấp xuống HTTP). Và dùng cert từ CA uy tín (Let's Encrypt miễn phí) cho dịch vụ công khai — không có lý do gì để dùng cert tự ký trên production công khai.
Cert pinning cho ứng dụng nhạy cảm — nhưng cân nhắc chi phí vận hành. Với app rất nhạy cảm (banking, messaging), có thể làm thêm certificate pinning: không chỉ tin "cert do CA hợp lệ ký" mà chỉ tin đúng cert/khoá cụ thể của server mình (phòng cả trường hợp một CA bị chiếm phát cert giả). Nhưng pinning là con dao hai lưỡi: khi server đổi cert (gia hạn, luân chuyển khoá), client pin cert cũ sẽ không kết nối được — cần quy trình cập nhật cẩn thận (pin nhiều cert, có cơ chế cập nhật). Chỉ dùng pinning khi lợi ích bảo mật xứng với chi phí vận hành tăng thêm.
Ba ý mang về
- HTTPS = mã hoá + xác thực, cả hai đều cần: tái hiện thật, client mặc định verify cert và từ chối cert tự ký với lỗi
x509: certificate signed by unknown authority— xác thực mới là thứ chống MITM, mã hoá mà không xác thực là vô nghĩa. - InsecureSkipVerify=true là bẫy chết người: tái hiện thật, nó cho kết nối thành công (đọc được dữ liệu nhạy cảm) nhưng chấp nhận bất kỳ cert nào — trông như sửa xong lỗi, thực ra mở cửa cho kẻ MITM giả server và giải mã toàn bộ lưu lượng; đừng bao giờ dùng nó, và nó hay bị để tạm rồi quên lên production.
- Cách đúng + phòng thủ theo lớp: cần tin cert riêng thì thêm vào
RootCAs(verify vẫn bật, chỉ tin cert chủ động thêm); ép TLS >= 1.2, bật HSTS, dùng cert từ CA uy tín; cert pinning cho app nhạy cảm nhưng cân nhắc chi phí xoay cert.
Nguồn
- Go — crypto/tls: Config (InsecureSkipVerify, RootCAs): https://pkg.go.dev/crypto/tls#Config
- OWASP — Transport Layer Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html
- MDN — HTTP Strict Transport Security (HSTS): https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
Phần sau ta bàn về quản lý bí mật: vì sao hardcode secret trong code và log secret ra là hai sai lầm phổ biến, cách đọc secret từ môi trường và che (redact) trong log.