Những điểm cần chú ý về bảo mật — không tin dữ liệu nhập vào, giữ kín thông tin bí mật

Bài viết này nằm trong khóa Kiến thức nền tảng về CNTT, xây dựng từ đầu những kiến thức CNTT thực tế tối thiểu mà bạn cần để lập trình và vibe coding.
Hai điều cơ bản là không tin dữ liệu người dùng nhập vào và giữ kín thông tin bí mật. Hình minh họa cho thấy SQL injection và XSS xảy ra như thế nào.

Bài viết này nói về hai trong số những điểm cần chú ý về bảo mật.

Một là không dùng nguyên chữ mà người dùng đã nhập, hai là không đặt giá trị bí mật vào những nơi có thêm nhiều người đọc được.

  • Vì sao kiểm tra dữ liệu đầu vào lại làm ở phía server
  • SQL injection xảy ra khi dữ liệu nhập vào được nối vào câu lệnh, và placeholder
  • XSS, ngăn được bằng escape ngay trước khi hiện ra màn hình
  • Những nơi bạn được phép đặt thông tin bí mật như API key và mật khẩu

Những chuyện này nếu xảy ra sau khi đã công khai thì đều không thể hoàn tác, và dữ liệu đã bị đọc thì không thu hồi lại được.

"Không kiểm tra dữ liệu đầu vào" nghĩa là dùng nguyên giá trị gửi tới

Kiểm tra dữ liệu đầu vào (input validation) là việc xác nhận giá trị gửi tới có đúng định dạng, độ dài và khoảng giá trị bạn dự tính hay không, trước khi dùng nó.

Bạn đặt điều kiện cho từng mục: tên tối đa 50 ký tự, số sao từ 1 đến 5.

Giá trị gửi tớiĐiều kiện đã đặt trướcViệc server làm
Ô tên: MinhTối đa 50 ký tự → khớpĐi tiếp tới bước lưu
Số sao: 7Số từ 1 đến 5 → không khớpTừ chối, không lưu
Nội dung: để trốngTừ 1 ký tự trở lên → không khớpTừ chối, không lưu

Chỉ hàng đầu tiên hợp điều kiện mới đi tiếp tới bước lưu; hai hàng còn lại không được lưu, và server trả về chỗ nào không khớp.

Thứ chạy phần kiểm tra ở màn hình là JavaScript (chương trình chạy bên trong trình duyệt) đã tới thiết bị của người dùng.

Người dùng tắt được nó, và cũng gửi thẳng lên server được mà không cần dùng trình duyệt.

Cùng một số 7 tách thành hai đường, tùy cách gửi
Gửi 7 vàosố saoGửi thẳngtừ formTắt kiểm tra ởmàn hình rồi gửiBị chặn ở kiểmtra màn hìnhTới server màkhông qua kiểm traBị server kiểmtra và từ chối
Một giá trị bên trái tách lên trên hoặc xuống dưới tùy cách gửi. Đường dưới không đi qua phần kiểm tra ở màn hình, nên nếu server không kiểm tra thì nó được lưu nguyên như vậy.

Phần kiểm tra ở màn hình là để người dùng nhập lại, và chỉ bước kiểm tra ở phía server mới quyết định giá trị có được dùng hay không.

Nếu lưu mà bỏ qua bước kiểm tra thì những giá trị không hợp điều kiện cứ thế nằm lại trong cơ sở dữ liệu.

Số sao ngoài khoảng 1 đến 5 làm sai lệch giá trị trung bình và thứ tự sắp xếp, còn giá trị không đọc được thành số thì làm dừng phần xử lý tổng hợp.

Muốn sửa những giá trị đã lọt vào thì bạn phải rà từng bản ghi còn lại một.

Giá trị gửi tới thì xác nhận ở server trước khi dùng

Kiểm tra dữ liệu đầu vào là việc xem giá trị gửi tới có hợp những điều kiện bạn đã đặt hay không, trước khi dùng nó.

Phần kiểm tra ở màn hình là để người dùng nhập lại ngay tại chỗ và có thể bị tắt trước khi gửi, nên bạn xác nhận đúng những điều đó thêm một lần nữa ở phía server.

Nối dữ liệu nhập vào vào câu lệnh thì chữ người ta nhập sẽ chạy như lệnh

Khi tìm các đánh giá đã lưu theo tên, server dựng nên một câu lệnh SQL (Structured Query Language. Ngôn ngữ truyền lệnh tới cơ sở dữ liệu).

Khi chữ nhập vào được nối vào chuỗi đó, toàn bộ được truyền đi như một chuỗi duy nhất.

Bên trong chuỗi duy nhất được truyền tới cơ sở dữ liệu
Chuỗi duy nhất mà cơ sở dữ liệu nhận được
Phần do lập trình viên viết
  • SELECT * FROM reviews WHERE name =
  • Lệnh lấy ra những hàng có name khớp, từ bảng tên là reviews
  • Dạng này đã được viết sẵn trong code
Phần đến từ ô tên
  • Minh — một cái tên bình thường
  • ' OR '1'='1 — chữ có chứa ký hiệu
  • Chữ nhập vào được đặt nguyên vẹn ở vị trí này
Khung ngoài là chuỗi duy nhất được truyền từ server tới cơ sở dữ liệu. Truyền đi rồi thì hai phần bên trong không phân biệt được nữa.

Truyền đi rồi thì phần do lập trình viên viết và phần đến từ ô tên là hai phần của cùng một chuỗi.

Cơ sở dữ liệu đọc cả chuỗi nhận được như một câu lệnh, gộp tất cả lại.

Chữ nhập vào ô tênCâu lệnh tạo ra sau khi nốiThứ cơ sở dữ liệu trả về
Minhname = 'Minh'Chỉ các đánh giá của Minh
Minh'name = 'Minh''Câu lệnh hỏng và báo lỗi
' OR '1'='1name = '' OR '1'='1'Mọi đánh giá

Việc chuỗi ký tự nhập vào được thực thi như một câu lệnh thay vì như một giá trị là injection, và loại xảy ra ở cơ sở dữ liệu là SQL injection.

Cách viết ngăn được điều đó là placeholder (chỉ đánh dấu bằng ký hiệu vị trí giá trị sẽ vào trong câu lệnh, còn giá trị thì truyền riêng).

Cùng một dữ liệu nhập vào tách thành hai, tùy cách viết ở phía server
Ở ô tên:' OR '1'='1Nối thẳng vàocâu lệnhTruyền lệnh vàgiá trị riêngĐọc cả chuỗinhư một câu lệnhChỉ tìm giá trịnhư một cái tênTrả về mọiđánh giáTrả về 0 dòng
Một dữ liệu nhập vào bên trái tách lên trên hoặc xuống dưới tùy cách viết. Khi lệnh và giá trị được truyền riêng, chữ có chứa ký hiệu cũng được xử lý như một cái tên.

Khi truyền riêng, cơ sở dữ liệu không đọc giá trị như lệnh mà tìm chuỗi đó như một cái tên rồi kết thúc với 0 dòng.

Kiểm tra là để xác nhận định dạng của giá trị, còn placeholder là để giá trị luôn được xử lý như giá trị dù định dạng thế nào.

Vai trò khác nhau nên bạn làm cả hai.

Không nối câu lệnh và chữ nhập vào thành một chuỗi

Khi chữ nhập vào được nối vào câu lệnh, cả ký hiệu người ta nhập cũng bị đọc như một phần của lệnh.

Truyền lệnh và giá trị riêng ra thì chữ có chứa ký hiệu cũng được xử lý như một cái tên bình thường, nên bạn giữ đủ cả hai: bước kiểm tra xác nhận định dạng, và cách viết xử lý giá trị như giá trị bất kể định dạng.

Không đổi ngay trước khi hiện ra thì bài đăng sẽ chạy trong trình duyệt của người dùng khác

Điều tương tự cũng xảy ra ở chỗ hiện bài đăng lên màn hình.

Việc chuỗi ký tự nhập vào được thực thi như JavaScript trong trình duyệt của người dùng khác là XSS (cross-site scripting).

Cùng một dữ liệu nhập vào có hai tên gọi, tùy chỗ nó trộn vào
Chữ người dùngđã nhậpTrộn vàocâu lệnh SQLCơ sở dữ liệu đọcnhư câu lệnhTruyền lệnh vàgiá trị riêngTrộn vàoHTML của trangTrình duyệt ngườidùng khác đọcEscape ngaytrước khi hiện
Bên trái là chữ người dùng đã nhập. Đường trên là khi nó trộn vào câu lệnh SQL, đường dưới là khi nó trộn vào HTML của màn hình. Bên đọc nó khác nhau, nên cách xử lý cũng khác nhau.

Cả hai đều giống nhau ở chỗ dữ liệu nhập vào bị đọc như lệnh, chỉ khác ở bên đọc nó.

Cách xử lý ở phía hiện ra màn hình là escape (chuyển những ký tự mang ý nghĩa đặc biệt sang dạng được hiển thị như ký tự bình thường).

Điểm phân định không phải lúc lưu, mà là ngay trước khi hiện ra màn hình.

Ký tự mang ý nghĩa đặc biệt trong HTMLCách viết sau khi thay thếKý tự hiện trên màn hình
<&lt;<
>&gt;>
&&amp;&
"&quot;"
'&#x27;'

Thay thế ngay trước khi hiện ra thì bài đăng vẫn chỉ là chữ

Nếu bạn hiện nguyên bài đăng lên màn hình thì thứ trộn bên trong nó sẽ chạy trong trình duyệt của người dùng khác.

Thứ chặn điều đó là phép thay thế được thực hiện ngay trước khi hiện ra, và vì chỉ các ký hiệu bị thay nên chữ mà người đọc nhìn thấy không đổi.

Giá trị bí mật thì chặn lại trước khi có thêm người đọc được

Điểm còn lại là nơi đặt thông tin bí mật.

Thứ bạn nhìn ở đây là nơi bạn đặt nó có bao nhiêu người đọc được.

Các file của my-app đi xa tới đâu
Thư mục làm việc trên máy bạn
Không ghi vào Git (chỉ còn lại trên máy bạn)
  • .env — giá trị của API key và mật khẩu
  • Các file cấu hình chỉ dùng trên máy bạn
Ghi vào Git (tới tay tất cả những người bạn đưa code)
  • server.js — code chạy trên server
  • .gitignore — danh sách các file không ghi lại
  • .env.example — bản mẫu chỉ viết tên
Tới trình duyệt (ai mở URL cũng đọc được)
  • index.html — màn hình đánh giá
  • script.js — code chạy trên màn hình
Khung ngoài là thư mục làm việc trên máy bạn. File nằm ở khung càng vào trong thì càng nhiều người đọc được, nên bạn không viết giá trị bí mật vào các khung bên trong.

Khung càng vào trong thì càng nhiều người đọc được: script.js thì ai cũng đọc được, còn server.js một khi đã ghi vào Git thì tất cả những người bạn đưa code đều đọc được.

Viết .env vào .gitignore (file liệt kê tên những file không cho Git ghi lại) thì các giá trị không vào lịch sử, nhưng

một khi đã ghi lại rồi thì chúng không mất khỏi lịch sử cũ, nên bạn tạo lại chính các giá trị đó.

File đã ghi vào Git thì đi xa tới đâu trên GitHub
Chỉ ghi lạicodeRepository côngkhai trên GitHubAi trên thế giớicũng đọc được.env cũng bịghi lạiCả file còn lạitrong lịch sửXóa rồi vẫn đọcđược từ lịch sửViết vào.gitignoreKhông vàolịch sửChỉ còn lạitrên máy bạn
Hàng trên là code đặt trong repository công khai, hàng giữa là khi .env cũng bị ghi lại, hàng dưới là khi nó được viết vào .gitignore. Từ trái sang phải, nơi bạn đặt nó quyết định ai đọc được.

Giá trị đã lọt vào repository công khai thì ai trên thế giới cũng đọc được.

Nơi đặt API key, và vòng những người đọc được nơi đó
Ai mở URL cũng đọc được
  • script.js — code tới trình duyệt
  • Phần đuôi của URL bạn gọi
  • Thông báo lỗi hiện trên màn hình
Tất cả những người bạn đưa code đều đọc được
  • server.js — code ghi vào Git
  • .env một khi đã ghi lại — còn trong lịch sử
Chỉ những người thao tác được trên server mới đọc được
  • Biến môi trường trên server — API key và mật khẩu nằm ở đây
Vòng càng ra ngoài thì càng nhiều người đọc được. Những nơi bạn được phép đặt giá trị chỉ là các nơi nằm trong vòng trong cùng.

Thứ duy nhất nằm trong vòng trong cùng là biến môi trường trên server.

Bạn không viết chúng vào những nơi ở hai vòng ngoài, cũng không viết vào log (bản ghi hoạt động mà server để lại), vì log đôi khi được mang ra ngoài.

my-app chạy bên trong khung bên phải đọc giá trị từ biến môi trường.

Giá trị đó không vào hai khung bên trái, nên số người đọc được không tăng lên.

Nơi đặt được quyết định bởi số người đọc được

Nơi bạn được phép đặt giá trị bí mật được quyết định bởi số người đọc được nơi đó.

File tới trình duyệt thì ai cũng đọc được, file ghi vào Git thì tới tay tất cả những người bạn đưa code, nên bạn không viết vào cả hai chỗ đó mà đặt vào biến môi trường trên server.

QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Giá trị của form đã được kiểm tra ở phía màn hình rồi, vì sao còn kiểm tra ở phía server nữa?

Câu 2Cách viết placeholder ngăn SQL injection thực chất làm gì?

Câu 3Đâu là nơi đúng để đặt API key của một dịch vụ bên ngoài?