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?
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ước | Việc server làm |
|---|---|---|
| Ô tên: Minh | Tối đa 50 ký tự → khớp | Đi tiếp tới bước lưu |
| Số sao: 7 | Số từ 1 đến 5 → không khớp | Từ chối, không lưu |
| Nội dung: để trống | Từ 1 ký tự trở lên → không khớp | Từ 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.
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.
- 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
- 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
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ên | Câu lệnh tạo ra sau khi nối | Thứ cơ sở dữ liệu trả về |
|---|---|---|
| Minh | name = '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'='1 | name = '' 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).
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ả 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 HTML | Cách viết sau khi thay thế | Ký tự hiện trên màn hình |
|---|---|---|
| < | < | < |
| > | > | > |
| & | & | & |
| " | " | " |
| ' | ' | ' |
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.
- .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
- 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
- index.html — màn hình đánh giá
- script.js — code chạy trên màn hình
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ị đó.
Giá trị đã lọt vào repository công khai thì ai trên thế giới 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
- server.js — code ghi vào Git
- .env một khi đã ghi lại — còn trong lịch sử
- Biến môi trường trên server — API key và mật khẩu nằm ở đây
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.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
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?