JavaRush/Java блог/Random UA/Безпека в Java: best practices
Roman Beekeeper
35 рівень

Безпека в Java: best practices

Стаття з групи Random UA
учасників
У серверних додатках один із найважливіших показників — це безпека. Це один із видів Non-Functional Requirements (нефункціональних вимог). Безпека в Java: best practices - 1Безпека включає безліч компонентів. Зрозуміло, щоб охопити повною мірою та об'ємом усі ті захисні принципи та дії, які відомі, потрібно написати не одну статтю, тож зупинимося на найважливішому. Людина, яка добре розуміється на цій темі, зможе налаштувати всі процеси і простежити, щоб вони не створювали нових дірок у безпеці, буде потрібна в будь-якій команді. Звичайно, не варто думати, що якщо дотримуватися цих практик, то додаток буде однозначно безпечним. Ні! Але воно точно буде безпечнішим з ними. Поїхали.

1. Забезпечити безпеку на рівні мови Java

Перш за все, безпека Java починається прямо на рівні можливостей мови. Ось що б ми робабо, якби не було модифікаторів доступу? Анархія, не інакше. Мова програмування допомагає нам писати безпечний код, а також користуватися багатьма неявними функціями безпеки:
  1. Сувора типізація. Java - це мова зі статичною типізацією, яка дає можливість виявлення помилок, пов'язаних з типами під час виконання.
  2. Модифікатори доступу. Завдяки їм ми можемо налаштовувати доступ до класів, методів та полів класів так, як нам потрібно.
  3. Автоматичне керування пам'яттю. Для цієї справи у нас (у джавістів;)) є Garbage Collector, який звільняє від налаштування вручну. Так, іноді виникають проблеми.
  4. Перевірка байткод: Java компілюється в байткод, який перевіряє runtime , перш ніж запустити його.
Крім того, є рекомендації від Оракла з безпеки. Звичайно написано не "високим складом" і можна заснути кілька разів при прочитанні, але воно варте того. Особливо важливим є документ Secure Coding Guidelines for Java SE , в якому є поради, як писати безпечний код. Цей документ має багато корисного. Якщо є можливість обов'язково варто прочитати. Для розігріву інтересу до цього матеріалу наведу кілька цікавих порад:
  1. Уникайте серіалізації чутливих до безпеки (secure-sensitive) класів. У цьому випадку можна отримати за серіалізованим файлом інтерфейс класу, не кажучи вже про серіалізовані дані.
  2. Намагайтеся уникати mutable класів для даних. Це дає всі переваги незмінних класів (наприклад, безпека потоку). Якщо об'єкт, що змінюється, це може призвести до несподіваної поведінки.
  3. Створюйте копії об'єктів, що змінюються. Якщо метод повертає посилання на внутрішній об'єкт, що змінюється, тоді клієнтський код може змінити внутрішній стан об'єкта.
  4. І так далі…
Загалом, у Secure Coding Guidelines for Java SE зібрано набір порад та рекомендацій щодо того, як правильно та безпечно писати код на Java.

2. Усунути SQL injection вразливість

Унікальна вразливість. Унікальність її полягає в тому, що вона одночасно і одна з найвідоміших, і одна з найчастіших вразливостей. Якщо не цікавитись питанням безпеки, то про це й не дізнаєшся. Що таке SQL injection? Це атака бази даних за допомогою впровадження додаткового SQL коду там, де це не очікується. Припустимо, ми маємо метод, який приймає якийсь параметр для запиту в базу даних. Наприклад, ім'я користувача. Код з уразливістю виглядатиме приблизно так:
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
   // Создается связь с базой данных
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Пишем sql запит в базу данных с нашим firstName
   String query = "SELECT * FROM USERS WHERE firstName = " + firstName;

   // выполняем запит
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   // при помощи mapToUsers переводит ResultSet в коллекцию юзеров.
   return mapToUsers(result);
}

private List<User> mapToUsers(ResultSet resultSet) {
   //переводит в коллекцию юзеров
}
У цьому прикладі SQL запит готується заздалегідь в окремому рядку. Здавалося б, у чому проблема, так? Може, проблема в тому, що краще б використати String.format? Ні? А в чому ж тоді? Поставимо себе місце тестувальника і подумаємо, що можна передати у значенні firstName. Наприклад:
  1. Можна передати те, що очікується, — ім'я користувача. Тоді база даних поверне всіх користувачів із таким ім'ям.
  2. Можна передати порожній рядок: тоді повернуться всі користувачі.
  3. А можна передати таке: “”; DROP TABLE USERS;”. І тут вже будуть великі проблеми. Цим запитом буде видалена таблиця з бази даних. З усіма даними. ВСІМИ.
Уявляєте, які проблеми можуть бути через це? Далі можна писати що завгодно. Можна змінити ім'я всіх користувачів, можна видалити їх адресаи. Простір для шкідництва великий. Щоб уникнути цього, потрібно припинити впровадження вже готового запиту і натомість формувати його за допомогою параметрів. Це має бути єдиний спосіб створення запитів до бази даних. У такий спосіб можна усунути цю вразливість. Приклад:
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
   // Создается связь с базой данных
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Создаем параметризированный запит.
   String query = "SELECT * FROM USERS WHERE firstName = ?";

   // Создаем подготовленный стейтмент с параметризованным запитом
   PreparedStatement statement = connection.prepareStatement(query);

   // Передаем значення параметра
   statement.setString(1, firstName);

   // выполняем запит
   ResultSet result = statement.executeQuery(query);

   // при помощи mapToUsers переводим ResultSet в коллекцию юзеров.
   return mapToUsers(result);
}

private List<User> mapToUsers(ResultSet resultSet) {
   //переводим в коллекцию юзеров
}
Таким чином уникають цієї вразливості. Для тих, кому хочеться поринути у питання глибше цієї статті – ось чудовий приклад . Як зрозуміти, що ви зрозуміли цю частину? Якщо жарт нижче став зрозумілим, то це вірна ознака, що суть вразливості ясна :D Безпека в Java: best practices - 2

3. Сканувати та тримати оновленими залежності

Що це означає? Для тих, хто не знає, що таке залежність (dependency), поясню: це jar архів з кодом, який підключають до проекту за допомогою систем автоматичного складання (Maven, Gradle, Ant) для того, щоб перевикористовувати чиєсь рішення. Наприклад, Project Lombok, який за нас генерує в рантаймі гетери, сетери і т.д. А якщо говорити про великі програми, то вони використовують безліч різних залежностей. Деякі транзитивно (тобто, у кожної залежності можуть бути свої залежності, і так далі). Тому зловмисники все частіше звертають увагу на open-source залежності, тому що їх регулярно використовують, і через них у багатьох клієнтів можуть бути проблеми. Важливо переконатися, що у всьому дереві залежностей (а саме так це і виглядає) немає відомих уразливостей. А для цього є кілька шляхів.

Використати Snyk для моніторингу

Інструмент Snyk перевіряє всі залежності проекту та відзначає відомі вразливості. Там можна зареєструватися та імпортувати свої проекти через GitHub, наприклад. Безпека в Java: best practices - 3Також, як видно з картинки вище, якщо в новішій версії є рішення цієї вразливості, Snyk запропонує зробити це і створити Pull-Request. Використовувати її можна безкоштовно для open-source проектів. Проекти скануватимуться з якоюсь періодичністю: раз на тиждень, раз на місяць. Я зареєструвався і додав усі свої публічні репозиторії у сканування Snyk (у цьому немає нічого небезпечного: вони й так у відкритому доступі для всіх). Далі Snyk показав результат сканування: Безпека в Java: best practices - 4А через деякий час, Snyk-bot підготував кілька Pull-Request'ів у проектах, де потрібно оновити залежності: Безпека в Java: best practices - 5І ось ще: Безпека в Java: best practices - 6Так що це чудовий інструмент для пошуку вразливостей та моніторингу щодо оновлення нових версій.

Використовувати GitHub Security Lab

Ті, хто працює GitHub, можуть скористатися та їх вбудованими інструментами. Більш детально про цей підхід можна почитати у моєму перекладі з їхнього блогу Анонс GitHub Security Lab . Цей інструмент, звичайно, простіше, ніж Snyk, але нехтувати ним точно не варто. Плюс до всього кількість відомих уразливостей тільки зростатиме, тому і Snyk, і GitHub Security Lab розширюватимуться і покращуватимуться.

Активувати Sonatype DepShield

Якщо користуватися GitHub'ом для зберігання своїх репозиторіїв, можна додати до себе на проекти з MarketPlace одну з програм - Sonatype DepShield. За його допомогою можна також сканувати проекти залежно. Більше того, якщо він знайде щось, буде створено GitHub Issue з відповідним описом, як показано нижче: Безпека в Java: best practices - 7

4. Обережно поводитися з конфіденційними даними

Безпека в Java: best practices - 8В англійській мові найчастіше зустрічається словосполучення “sensitive data”. Розкриття персональних даних, номерів кредитних карток та іншої особистої інформації клієнта може завдати непоправної шкоди. Насамперед, потрібно уважно подивитися на дизайн програми та визначити, чи дійсно потрібні якісь дані. Можливо, щодо них жодної потреби немає, а додали їх для майбутнього, яке не настало і навряд чи настане. До того ж, під час логування проекту може статися витік таких даних. Простий спосіб запобігти попаданню конфіденційних даних у ваші логи – очистити методи toString()доменних сутностей (таких як User, Student, Teacher тощо). Таким чином, не можна буде роздрукувати конфіденційні поля випадково. Якщо використовувати Lombok для генерації методу toString(), то можна використовувати інструкцію@ToString.Excludeщоб поле не використовувалося у виведенні через метод toString(). Крім того, будьте дуже обережні, передаючи дані до зовнішнього світу. Наприклад, є http endpoint, який відображає імена всіх користувачів. Не потрібно показувати внутрішній унікальний ідентифікатор користувача. Чому? Тому що по ньому зловмисник може отримати іншу, більш конфіденційну інформацію про кожного користувача. Наприклад, якщо задіяти Jackson для серіалізації та десеріалізації POJO у JSON , то можна використовувати анотації @JsonIgnoreта @JsonIgnoreProperties, щоб виключити серіалізацію та десеріалізацію конкретних полів. А взагалі потрібно використовувати різні класи POJO для різних місць. Що це означає?
  1. Для роботи з базою даних використовувати одні POJO – Entity.
  2. Для роботи з бізнес логікою перекладати Entity в Model.
  3. Для роботи із зовнішнім світом та з відправкою на http запити – використовувати треті сутності – DTO.
Таким чином, можна чітко визначити, які саме поля будуть видно зовні, а які — ні.

Використовувати надійні алгоритми шифрування та хешування

Конфіденційні дані клієнтів слід зберігати надійно. Для цього необхідно використовувати шифрування. Залежно від завдання потрібно вирішити, який тип шифрування використовувати. Далі, сильніше шифрування вимагає більше часу, тому знову ж таки потрібно враховувати, наскільки його необхідність виправдовує витрачений на цей час. Зрозуміло, можна написати алгоритм самому. Але це зайве. Можна скористатися вже існуючими рішеннями у цій галузі. Наприклад, 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>
Подивимося, як ним користуватися, на прикладі того, як провести шифрування в один і інший бік:
private static void encryptDecryptExample() {
   AeadConfig.register();
   KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);

   String plaintext = "Цой жив!";
   String aad = "Юрий Клинских";

   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));
}

Шифрування паролів

Для цього завдання найбезпечніше використовувати асиметричне шифрування. Чому? Тому що в програмі реально не потрібно розшифровувати паролі назад. Такий загальний підхід. Насправді, коли користувач вводить пароль, система шифрує його і порівнює з тим, що лежить у сховищі паролів. Шифрування проходить одним і тим же засобом, тому можна очікувати, що вони збігатимуться (при введенні правильного пароля;), зрозуміло). Для цієї справи підходять BCrypt та SCrypt. Обидва є односторонніми функціями (криптографічними хешами) з обчислювально-складними алгоритмами, які займають багато часу. Це саме те, що потрібно, тому що розшифровка в лоба займе цілу вічність. Наприклад, Spring Security підтримує низку алгоритмів. Можна скористатися SCryptPasswordEncoderіBCryptPasswordEncoder. Те, що зараз є сильним алгоритмом шифрування, наступного року може бути слабким. Внаслідок цього робимо висновок, що потрібно перевіряти алгоритми, що використовуються, і оновлювати бібліотеки з алгоритмами.

Замість виведення

Сьогодні ми поговорабо про безпеку і, зрозуміло, багато речей залишилося за кадром. Я лише прочинив вам двері в новий світ: світ, який живе своїм життям. З безпекою так само, як із політикою: якщо ви не займатиметеся політикою, політика займеться вами. Традиційно пропоную підписатися на мій гітхаб аккаунт . Я там викладаю свої напрацювання за різними технологіями, які вивчаю та застосовую на роботі.

Корисні посилання

Так, майже всі статті на читати написані англійською. Хочемо ми цього чи ні, але англійська це мова для комунікації програмістів. Усі нові статті, книги, журнали з програмування пишуть англійською. Тому і посилання мої на рекомендації в основному англійською:
  1. Хабр: SQL Injection для початківців
  2. Oracle: Java Security Resource Center
  3. Oracle: Secure Coding Guidelines for Java SE
  4. Baeldung: The Basics of Java Security
  5. Medium: 10 tips для Power-up вашої Java Security
  6. Snyk: 10 java security best practices
  7. JR: Анонс GitHub Security Lab: захист всього коду разом
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.