CodeGym /Blog Java /Ngẫu nhiên /Bảo mật trong Java: các phương pháp hay nhất
John Squirrels
Mức độ
San Francisco

Bảo mật trong Java: các phương pháp hay nhất

Xuất bản trong nhóm
Một trong những số liệu quan trọng nhất trong các ứng dụng máy chủ là bảo mật. Đây là một loại yêu cầu phi chức năng . Bảo mật trong Java: các phương pháp hay nhất - 1Bảo mật bao gồm nhiều thành phần. Tất nhiên, sẽ cần nhiều hơn một bài viết để trình bày đầy đủ tất cả các nguyên tắc bảo mật và biện pháp bảo mật đã biết, vì vậy chúng tôi sẽ tập trung vào phần quan trọng nhất. Một người thành thạo về chủ đề này có thể thiết lập tất cả các quy trình có liên quan, tránh tạo ra các lỗ hổng bảo mật mới và sẽ cần thiết cho bất kỳ nhóm nào. Tất nhiên, bạn không nên nghĩ rằng ứng dụng của mình sẽ an toàn 100% nếu bạn làm theo các phương pháp này. KHÔNG! Nhưng nó chắc chắn sẽ an toàn hơn với họ. Đi nào.

1. Cung cấp bảo mật ở cấp độ ngôn ngữ Java

Trước hết, bảo mật trong Java bắt đầu ngay ở cấp độ khả năng của ngôn ngữ. Chúng ta sẽ làm gì nếu không có công cụ sửa đổi truy cập? Sẽ không có gì ngoài tình trạng vô chính phủ. Ngôn ngữ lập trình giúp chúng tôi viết mã an toàn và cũng sử dụng nhiều tính năng bảo mật ngầm định:
  1. gõ mạnh. Java là một ngôn ngữ được gõ tĩnh. Điều này giúp có thể phát hiện các lỗi liên quan đến loại trong thời gian chạy.
  2. Công cụ sửa đổi truy cập. Những điều này cho phép chúng tôi tùy chỉnh quyền truy cập vào các lớp, phương thức và trường khi cần.
  3. Quản lý bộ nhớ tự động. Đối với điều này, các nhà phát triển Java có một trình thu gom rác giúp chúng tôi không phải định cấu hình mọi thứ theo cách thủ công. Vâng, đôi khi vấn đề phát sinh.
  4. Xác minh mã byte : Java được biên dịch thành mã byte, được bộ thực thi kiểm tra trước khi nó được thực thi.
Ngoài ra, còn có các khuyến nghị bảo mật của Oracle . Tất nhiên, nó không được viết bằng ngôn ngữ cao sang và bạn có thể ngủ gật nhiều lần khi đọc nó, nhưng nó đáng giá. Đặc biệt, tài liệu có tiêu đề Hướng dẫn mã hóa an toàn cho Java SE rất quan trọng. Nó cung cấp lời khuyên về cách viết mã an toàn. Tài liệu này truyền tải một lượng lớn thông tin rất hữu ích. Nếu bạn có cơ hội, bạn chắc chắn nên đọc nó. Để thu hút sự quan tâm của bạn đối với tài liệu này, dưới đây là một số mẹo thú vị:
  1. Tránh tuần tự hóa các lớp nhạy cảm với bảo mật. Tuần tự hóa hiển thị giao diện lớp trong tệp được tuần tự hóa, chưa kể đến dữ liệu được tuần tự hóa.
  2. Cố gắng tránh các lớp có thể thay đổi đối với dữ liệu. Điều này cung cấp tất cả các lợi ích của các lớp bất biến (ví dụ: an toàn luồng). Nếu bạn có một đối tượng có thể thay đổi, nó có thể dẫn đến hành vi không mong muốn.
  3. Tạo bản sao của các đối tượng có thể thay đổi được trả về. Nếu một phương thức trả về một tham chiếu đến một đối tượng có thể thay đổi bên trong, thì mã máy khách có thể thay đổi trạng thái bên trong của đối tượng.
  4. Và như thế…
Về cơ bản, Nguyên tắc mã hóa an toàn cho Java SE là một tập hợp các mẹo và thủ thuật về cách viết mã Java chính xác và an toàn.

2. Loại bỏ các lỗ hổng SQL injection

Đây là một loại lỗ hổng đặc biệt. Nó đặc biệt bởi vì nó vừa là một trong những lỗ hổng nổi tiếng nhất vừa là một trong những lỗ hổng phổ biến nhất. Nếu bạn chưa bao giờ quan tâm đến bảo mật máy tính, thì bạn sẽ không biết về nó. SQL injection là gì? Đây là một cuộc tấn công cơ sở dữ liệu liên quan đến việc tiêm mã SQL bổ sung vào nơi nó không được mong đợi. Giả sử chúng ta có một phương thức chấp nhận một số loại tham số để truy vấn cơ sở dữ liệu. Ví dụ: tên người dùng. Mã dễ bị tổn thương sẽ giống như thế này:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
  
   // Compose a SQL database query with our firstName
   String query = "SELECT * FROM USERS WHERE firstName = " + firstName;
  
   // Execute the query
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
Trong ví dụ này, một truy vấn SQL được chuẩn bị trước trên một dòng riêng biệt. Vì vậy, vấn đề là gì, phải không? Có lẽ vấn đề là sẽ tốt hơn nếu sử dụng String.format ? KHÔNG? Chà, sau đó thì sao? Hãy đặt mình vào vị trí của người thử nghiệm và suy nghĩ về những gì có thể được chuyển thành giá trị của firstName . Ví dụ:
  1. Chúng tôi có thể vượt qua những gì được mong đợi - một tên người dùng. Sau đó, cơ sở dữ liệu sẽ trả về tất cả người dùng có tên đó.
  2. Chúng ta có thể truyền một chuỗi rỗng. Sau đó, tất cả người dùng sẽ được trả lại.
  3. Nhưng chúng ta cũng có thể truyền như sau: "'; DROP TABLE USERS;". Và ở đây bây giờ chúng ta có vấn đề huuuuuuge. Truy vấn này sẽ xóa một bảng khỏi cơ sở dữ liệu. Cùng với tất cả các dữ liệu. TẤT CẢ.
Bạn có thể tưởng tượng những vấn đề này sẽ gây ra? Ngoài ra, bạn có thể viết bất cứ điều gì bạn muốn. Bạn có thể thay đổi tên của tất cả người dùng. Bạn có thể xóa địa chỉ của họ. Phạm vi phá hoại là vô cùng lớn. Để tránh điều này, bạn cần ngăn chặn việc đưa truy vấn tạo sẵn và thay vào đó tạo truy vấn bằng các tham số. Đây phải là cách duy nhất để tạo các truy vấn cơ sở dữ liệu. Đây là cách bạn có thể loại bỏ lỗ hổng này. Ví dụ:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Create a parameterized query.
   String query = "SELECT * FROM USERS WHERE firstName = ?";

   // Create a prepared statement with the parameterized query
   PreparedStatement statement = connection.prepareStatement(query);
  
   // Pass the parameter's value
   statement.setString(1, firstName);

   // Execute the query
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
Bằng cách này, lỗ hổng được tránh. Đối với những người muốn tìm hiểu sâu hơn về bài viết này, đây là một ví dụ tuyệt vời . Làm thế nào để bạn biết khi bạn hiểu lỗ hổng này? Nếu bạn gặp câu nói đùa trong truyện tranh dưới đây thì chắc hẳn bạn đã nắm rõ lỗ hổng này là gì rồi đấy :DBảo mật trong Java: các phương pháp hay nhất - 2

3. Quét các phần phụ thuộc và cập nhật chúng

Điều đó nghĩa là gì? Nếu bạn không biết phụ thuộc là gì, tôi sẽ giải thích. Phần phụ thuộc là kho lưu trữ JAR có mã được kết nối với dự án bằng cách sử dụng hệ thống xây dựng tự động (Maven, Gradle, Ant) để sử dụng lại giải pháp của người khác. Ví dụ: Project Lombok , tạo getters, setters, v.v. cho chúng tôi trong thời gian chạy. Các ứng dụng lớn có thể có rất nhiều phụ thuộc. Một số là bắc cầu (nghĩa là mỗi phụ thuộc có thể có các phụ thuộc riêng của nó, v.v.). Do đó, những kẻ tấn công ngày càng chú ý đến các phụ thuộc nguồn mở, vì chúng được sử dụng thường xuyên và nhiều khách hàng có thể gặp sự cố vì chúng. Điều quan trọng là phải đảm bảo rằng không có lỗ hổng nào đã biết trong toàn bộ cây phụ thuộc (vâng, nó trông giống như một cái cây). Có nhiều hướng khác nhau để làm điều đó.

Sử dụng Snyk để theo dõi sự phụ thuộc

Snyk kiểm tra tất cả các thành phần phụ thuộc của dự án và gắn cờ các lỗ hổng đã biết. Bạn có thể đăng ký trên Snyk và nhập các dự án của mình qua GitHub. Bảo mật trong Java: các phương pháp hay nhất - 3Ngoài ra, như bạn có thể thấy từ hình trên, nếu lỗ hổng được khắc phục trong phiên bản mới hơn, thì Snyk sẽ cung cấp bản sửa lỗi và tạo yêu cầu kéo. Bạn có thể sử dụng nó miễn phí cho các dự án mã nguồn mở. Các dự án được quét định kỳ, ví dụ: mỗi tuần một lần, mỗi tháng một lần. Tôi đã đăng ký và thêm tất cả các kho lưu trữ công khai của mình vào quá trình quét Snyk (không có gì nguy hiểm về việc này vì chúng đã được công khai cho mọi người). Sau đó, Snyk hiển thị kết quả quét: Bảo mật trong Java: các phương pháp hay nhất - 4Và sau một thời gian, Snyk-bot đã chuẩn bị một số yêu cầu kéo trong các dự án mà các phần phụ thuộc cần được cập nhật: Bảo mật trong Java: các phương pháp hay nhất - 5Ngoài ra:Bảo mật trong Java: các phương pháp hay nhất - 6Đây là một công cụ tuyệt vời để tìm lỗ hổng và theo dõi cập nhật các phiên bản mới.

Sử dụng Phòng thí nghiệm bảo mật GitHub

Bất kỳ ai làm việc trên GitHub đều có thể tận dụng các công cụ tích hợp sẵn của nó. Bạn có thể đọc thêm về cách tiếp cận này trong bài đăng trên blog của họ có tựa đề Thông báo Phòng thí nghiệm bảo mật GitHub . Tất nhiên, công cụ này đơn giản hơn Snyk, nhưng bạn chắc chắn không nên bỏ qua nó. Hơn nữa, số lượng lỗ hổng đã biết sẽ chỉ tăng lên, vì vậy cả Snyk và GitHub Security Lab sẽ tiếp tục mở rộng và cải thiện.

Kích hoạt Sonatype DepShield

Nếu bạn sử dụng GitHub để lưu trữ kho lưu trữ của mình, bạn có thể thêm Sonatype DepShield, một trong những ứng dụng trong MarketPlace, vào dự án của mình. Nó cũng có thể được sử dụng để quét các dự án phụ thuộc. Ngoài ra, nếu nó tìm thấy thứ gì đó, Vấn đề GitHub sẽ được tạo với mô tả phù hợp như hiển thị bên dưới:Bảo mật trong Java: các phương pháp hay nhất - 7

4. Xử lý dữ liệu bí mật một cách cẩn thận

Ngoài ra, chúng tôi có thể sử dụng cụm từ "dữ liệu nhạy cảm". Rò rỉ thông tin cá nhân, số thẻ tín dụng và các thông tin nhạy cảm khác của khách hàng có thể gây ra tác hại không thể khắc phục được. Trước hết, hãy xem kỹ thiết kế ứng dụng của bạn và xác định xem bạn có thực sự cần dữ liệu này hay dữ liệu kia không. Có lẽ bạn không thực sự cần một số dữ liệu bạn có - dữ liệu được thêm vào cho một tương lai chưa đến và không chắc sẽ đến. Ngoài ra, nhiều bạn vô tình làm rò rỉ dữ liệu đó thông qua ghi nhật ký. Một cách dễ dàng để ngăn dữ liệu nhạy cảm xâm nhập nhật ký của bạn là loại bỏ các phương thức toString() của các thực thể miền (chẳng hạn như Người dùng, Sinh viên, Giáo viên, v.v.). Điều này sẽ ngăn bạn vô tình xuất ra các trường bí mật. Nếu bạn sử dụng Lombok để tạo toString()bạn có thể sử dụng chú thích @ToString.Exclude để ngăn một trường được sử dụng trong đầu ra của phương thức toString() . Ngoài ra, hãy thật cẩn thận khi gửi dữ liệu ra thế giới bên ngoài. Giả sử chúng ta có một điểm cuối HTTP hiển thị tên của tất cả người dùng. Không cần hiển thị ID nội bộ duy nhất của người dùng. Tại sao? Vì kẻ tấn công có thể sử dụng nó để lấy thông tin khác, nhạy cảm hơn về người dùng. Ví dụ: nếu bạn sử dụng Jackson để tuần tự hóa/giải tuần tự hóa một POJO đến/từ JSON , thì bạn có thể sử dụng @JsonIgnore@JsonIgnorePropertieschú thích để ngăn tuần tự hóa/giải tuần tự hóa các trường cụ thể. Nói chung, bạn cần sử dụng các lớp POJO khác nhau ở những nơi khác nhau. Điều đó nghĩa là gì?
  1. Khi làm việc với cơ sở dữ liệu, hãy sử dụng một loại POJO (một thực thể).
  2. Khi làm việc với logic nghiệp vụ, hãy chuyển đổi một thực thể thành một mô hình.
  3. Khi làm việc với thế giới bên ngoài và gửi yêu cầu HTTP, hãy sử dụng các thực thể (DTO) khác nhau.
Bằng cách này, bạn có thể xác định rõ trường nào sẽ hiển thị từ bên ngoài và trường nào sẽ không.

Sử dụng thuật toán mã hóa và băm mạnh

Dữ liệu bí mật của khách hàng phải được lưu trữ an toàn. Để làm điều này, chúng ta cần sử dụng mã hóa. Tùy thuộc vào nhiệm vụ, bạn cần quyết định sử dụng loại mã hóa nào. Ngoài ra, mã hóa mạnh hơn cần nhiều thời gian hơn, vì vậy, một lần nữa, bạn cần xem xét mức độ cần thiết của mã hóa so với thời gian dành cho mã hóa đó. Tất nhiên, bạn có thể tự viết thuật toán mã hóa. Nhưng điều này là không cần thiết. Bạn có thể sử dụng các giải pháp hiện có trong lĩnh vực này. Ví dụ: Google Tink :

<!-- https://mvnrepository.com/artifact/com.google.crypto.tink/tink -->
<dependency>
   <groupid>com.google.crypto.tink</groupid>
   <artifactid>tink</artifactid>
   <version>1.3.0</version>
</dependency>
Hãy xem phải làm gì, sử dụng ví dụ này liên quan đến mã hóa và giải mã:

private static void encryptDecryptExample() {
   AeadConfig.register();
   KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);

   String plaintext = "Elvis lives!";
   String aad = "Buddy Holly";

   Aead aead = handle.getPrimitive(Aead.class);
   byte[] encrypted = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encryptedString = Base64.getEncoder().encodeToString(encrypted);
   System.out.println(encryptedString);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encrypted), aad.getBytes());
   System.out.println(new String(decrypted));
}

Mã hóa mật khẩu

Đối với nhiệm vụ này, cách an toàn nhất là sử dụng mã hóa bất đối xứng. Tại sao? Vì ứng dụng không thực sự cần giải mã mật khẩu. Đây là cách tiếp cận tiêu chuẩn. Trong thực tế, khi người dùng nhập mật khẩu, hệ thống sẽ mã hóa nó và so sánh nó với những gì tồn tại trong kho lưu trữ mật khẩu. Quá trình mã hóa tương tự được thực hiện, vì vậy chúng tôi có thể mong đợi rằng chúng sẽ khớp, tất nhiên là nếu nhập đúng mật khẩu :) BCrypt và SCrypt phù hợp ở đây. Cả hai đều là các hàm một chiều (băm mật mã) với các thuật toán tính toán phức tạp, mất nhiều thời gian. Đây chính xác là những gì chúng ta cần, vì các tính toán trực tiếp sẽ mất mãi mãi (à, một thời gian dài, rất lâu). Spring Security hỗ trợ toàn bộ các thuật toán. Chúng ta có thể sử dụng SCryptPasswordEncoderBCryptPasswordEncoder. Những gì hiện được coi là thuật toán mã hóa mạnh có thể bị coi là yếu vào năm tới. Do đó, chúng tôi kết luận rằng chúng tôi nên thường xuyên kiểm tra các thuật toán chúng tôi sử dụng và cập nhật các thư viện chứa các thuật toán mã hóa nếu cần.

Thay cho lời kết

Hôm nay chúng ta đã nói về vấn đề bảo mật và, một cách tự nhiên, rất nhiều thứ đã bị bỏ lại phía sau hậu trường. Tôi vừa mở ra cánh cửa đến một thế giới mới cho bạn, một thế giới có cuộc sống của riêng nó. An ninh cũng giống như chính trị: nếu bạn không bận rộn với chính trị, chính trị sẽ tự bận rộn với bạn. Theo truyền thống, tôi khuyên bạn nên theo dõi tôi trên tài khoản GitHub . Ở đó, tôi đăng những sáng tạo của mình liên quan đến các công nghệ khác nhau mà tôi đang nghiên cứu và áp dụng tại nơi làm việc.

Liên kết hữu ích

  1. Guru99: Hướng dẫn tiêm SQL
  2. Oracle: Trung tâm tài nguyên bảo mật Java
  3. Oracle: Nguyên tắc mã hóa an toàn cho Java SE
  4. Baeldung: Khái niệm cơ bản về bảo mật Java
  5. Trung bình: 10 mẹo để tăng cường bảo mật Java của bạn
  6. Snyk: 10 phương pháp hay nhất về bảo mật Java
  7. GitHub: Thông báo GitHub Security Lab: cùng nhau bảo vệ mã của thế giới
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION