Bạn đã từng gặp những rắc rối khi bỏ qua những nguyên tắc về an toàn trong phát triển ứng dụng? Phần mềm của bạn thường bị phát hiện ra các lỗ hổng an ninh sau khi phát hành? v.v. Mời bạn đọc bài viết sau đây của tác giả Nguyễn Hiển để rõ hơn về những vấn đề nêu trên.
Thế nào là lập trình an toàn?
Có lẽ nên bắt đầu với khái niệm một hệ thống “an toàn” hay “bảo mật” (security). Một hệ thống được coi là “an toàn” khi nó đảm bảo được ba yếu tố chính (thường được gọi là tam giác CIA) bao gồm: Confidentiality (tuyệt mật), Integrity (toàn vẹn) và Availability (sẵn sàng). Để hệ thống an toàn đòi hỏi rất nhiều công sức trong tất cả giai đoạn phát triển phần mềm, trong phạm vi bài viết này tôi chỉ đề cập đến khái niệm an toàn trong giai đoạn lập trình.
Tôi chỉ cố gắng đưa ra một số sai lầm thường gặp nhất trong việc lập trình gây ảnh hưởng tới tính an toàn của hệ thống, đây cũng là những chỉ dẫn đơn giản giúp bạn hiểu về một công việc cần làm để có “good code”.
HTTP method
Form là công cụ hay dùng nhất để client gửi dữ liệu tới server thông qua một trong hai phương thức: GET hoặc POST. Điểm khác nhau cơ bản là GET gửi dữ liệu qua URL, POST thì không.
Điều gì xảy ra nếu chúng ta dùng GET? Một người đứng phía sau có thể nhìn thấy thông tin tài khoản của người dùng khi họ login như:
Vậy tại sao chúng ta vẫn dùng GET? Vì tiện. Các lập trình viên thường có xu hướng dùng GET khi lập trình để tiện thay đổi thông tin cho việc test, nhằm tăng năng suất. Vấn đề là họ thường quên điều chỉnh khi release sản phẩm.
Giải pháp: Luôn sử dụng POST với những thông tin nhạy cảm, và hãy luôn nhớ kiểm tra phương thức HTTP trong các form trước khi đóng gói sản phẩm.
Vi phạm: Tính tuyệt mật
SQL injection
Đây là lỗi kinh điển nhưng có lẽ bạn đã và vẫn đang mắc phải. Giả sử, hệ thống cho phép đăng nhập qua username và password, tôi nghĩ đây là cách bạn đang làm:
– Nhận dữ liệu và lưu vào 2 biến, giả sử usr và pwd
– Tạo câu lệnh sql = “SELECT * FROM table WHERE username = ‘“ + usr + “’ AND password = ‘” + pwd + ’“; (với table, username, password là bảng và các trường trong DB)
– Thực thi câu lệnh, nếu (giả sử) ResultSet trả về có nhiều hơn 0 bản ghi, người dùng được coi là nhập đúng thông tin và đăng nhập thành công.
OK? Điều gì xảy ra nếu người dùng nhập:
– usr: admin
– pwd: ’ OR ‘1’ = ‘1
Khi đó, câu lệnh SQL trở thành:
SELECT * FROM table WHERE username = ‘admin’ AND password = ‘’ OR ‘1’ = ‘1’
Và thường thì sẽ có nhiều hơn 0 bản ghi (chính xác là toàn bộ bản ghi trong bảng table) được trả về, người dùng đăng nhập thành công.
Và khi pwd là: ’ OR ‘1’ = ‘1’; UPDATE users SET password = ‘123’ WHERE username = ‘admin’;
Câu lệnh SQL trở thành:
SELECT * FROM table WHERE username = ‘admin’ AND password = ‘’ OR ‘1’ = ‘1’; UPDATE users SET password = ‘123’ WHERE username = ‘admin’;
Hai câu lệnh SQL được thực thi và password của tài khoản admin được đặt lại thành ‘123’.
Lỗi này đặc biệt nghiêm trọng trong những hệ thống mã nguồn mở hoặc trên các diễn đàn, vì tên đăng nhập của người dùng và cấu trúc DB thường được biết trước.
Giải pháp: Chắc bạn cũng thấy, SQL injection lợi dụng những ký tự đặc biệt ‘ trong chuỗi giá trị nhập vào nên giải pháp đơn giản là hãy luôn loại bỏ chúng trước khi chuyển thành câu lệnh SQL. Do đây là một lỗi phổ biến nên hầu hết ngôn ngữ hiện đại đều cung cấp khả năng xử lý. Trong Java, khi làm việc với JDBC, thay vì dùng Statement, hãy sử dụng PreparedStatement hoặc CallableStatement. (http://docs.oracle.com/javase/6/docs/api/java/sql/PreparedStatement.html)
Tham khảo thêm tại:
- http://www.unixwiz.net/techtips/sql-injection.html
- https://www.guru99.com/learn-sql-injection-with-practical-example.html
Vi phạm: Tính tuyệt mật, toàn vẹn
Exception
Ai cũng biết, và có lẽ dù có nhắc đến lỗi này cả triệu lần nữa thì chúng ta vẫn có thể mắc phải. Đơn giản vì chúng ta thường lập trình rất nhanh để sớm đưa ra sản phẩm và hay bỏ qua việc tung (throw) và bắt (catch) exception.
Chúng ta xây dựng một chương trình bảng tính tuyệt vời với rất nhiều chức năng và khi người dùng nhập phép toán 1/0, toàn bộ chương trình ngừng hoạt động; tất cả dữ liệu trước đó bị mất do không bắt exception. Các IDE ngày nay rất thông minh, nếu chúng ta viết câu lệnh a = 1/0; Netbeans chắc chắn sẽ bắt chúng ta phải catch exception. Vì vậy, các lập trình viên thường khá chủ quan. Nhưng IDE sẽ bất lực nếu chúng ta viết a = b / c; với c = 0. Đôi khi cả hệ thống sụp đổ chỉ do 1 sự bất cẩn nhỏ trong lập trình. Điều này còn nguy hại hơn nếu bạn xây dựng 1 ứng dụng web server, khi hàng ngàn request có thể không được xử lý chỉ do lỗi của 1 request. Ứng dụng như vậy không được coi là có khả năng chịu lỗi (failure tolerant), ảnh hưởng tới tính sẵn sàng. Nguy hiểm hơn, một số người có thể đọc được lỗi và khai thác lỗ hổng (vulnerability) của hệ thống.
Giải pháp: Hãy luôn bắt exception với bất kỳ thao tác nào có khả năng xảy ra lỗi như: Các phép toán (chia 0, tràn số (c = a * b với a, b có kiểu int có thể vượt ngoài khoảng kiểu int của c là sai), phép trừ ra số âm…), đọc hay ghi dữ liệu vào file (file có thể không tồn tại, không có quyền đọc/ghi), truyền dữ liệu qua mạng (không có kết nối…), sử dụng object chưa được khởi tạo (null pointer exception – lỗi rất phổ biến)…
Đồng thời, hãy luôn tung exception trong các phương thức có khả năng gây lỗi do mình tự viết.
Hãy nhớ rằng, chúng ta không đủ thời gian để kiểm thử (test) tất cả trường hợp và thường chọn trường hợp “rất đẹp” trên những bộ dữ liệu “rất đẹp” khi chào hàng phần mềm. Thực tế, người dùng lại luôn “ngớ ngẩn” và luôn gây lỗi, đừng để một hệ thống hoành tráng bị sụp đổ bởi sự vô trách nhiệm nhỏ.
Vi phạm: Tính tuyệt mật, tính toàn vẹn, tính sẵn sàng.
Duyệt file
Một ứng dụng có thiết kế tốt cũng có thể hay mắc phải lỗi này. Chúng ta tạo ra một template và hiển thị nội dung file theo ngữ cảnh người dùng. Ví dụ, khi người dùng vào trang đăng nhập, bạn sẽ sử dụng URL: http://myweb.com?view=login.html, nhận biết trang cần hiển thị qua parameter view, đọc file login.html và trả về client. Điều gì xảy ra nếu người dùng nhập URL: http://myweb.com?view=../../data/document.doc? Nếu không được phân quyền tốt, có thể bạn sẽ trả về file document.doc nằm trong thư mục data trên ổ cứng.
Giải pháp: Phân quyền cho thư mục và thực hiện rewrite URL là cách tốt nhất, song việc này thường được thực hiện tại bước triển khai. Và nếu bạn không chắc việc triển khai chính xác, hãy luôn kiểm tra tính hợp lệ của các file được trả về.
Vi phạm: Tính tuyệt mật, tính toàn vẹn, tính sẵn sàng.
Và?
Đến đây, có thể bạn đã nhận thấy ba yếu tố trong tam giác CIA nói chung không thể cùng đạt điểm tối đa. Đơn giản vì từng yếu tố này vốn tự mâu thuẫn lẫn nhau. Ví dụ, để tăng cường tính tuyệt mật, hệ thống ngân hàng yêu cầu chúng ta nhập mật mã (được gửi qua SMS) mỗi khi chuyển tiền; gây ảnh hưởng tới tính sẵn sàng của hệ thống do những người quên điện thoại sẽ không thực hiện được giao dịch. Vậy đâu là mức phù hợp cho từng tiêu chí của hệ thống?
Trên đây chỉ là một số rất nhỏ những sai lầm có thể gặp phải trong việc lập trình gây ảnh hưởng tới tính an toàn của hệ thống. Bạn có thể tìm hiểu thêm về những sai lầm thường gặp khác, hoặc tôi sẽ đề cập tiếp nội dung này trong một lần thích hợp.
Theo Nguyễn Hiển – FPT University – Tạp chí lập trình