SQL-инъекции или нарушение в целостности структуры SQL-запроса являются одними из самых распространённых и уязвимостей в вопросе безопасности. Используя SQL-инъекции злоумышленники могут получить полный доступ к базам данных, персональным данным пользователей, могут удалить или изменить данные и даже таблицы.
В основном опасность SQL-инъекций возникает при динамической генерации запросов к базе и использовании пользовательских данных.
Пусть у нас есть таблица с заказами и мы будем реализовывать в ней поиск по названию:
public List<Orders> findOrdersByName(String enteredName) throws SQLException {
String sql = "select * from orders "
+ " where name like '"
+ enteredName
+ "'";
Connection c = dataSource.getConnection();
return c.createStatement().executeQuery(sql);
}
Приведенный выше пример - плох. В этом случае злоумышленник имеет доступ к базам через поле enteredName. Например, введя в поле строку
’; drop table orders; --
можно совсем удалить таблицу - после объединения одинарная кавычка в начале будет соответствовать той, которая уже есть в запросе. Два тире в конце означают, что все после них будет интерпретироваться как комментарий. Между - можно написать любой запрос. Таким образом, окончательный запрос будет таким
select * from orders where name like ' ’ ; drop table orders; -- '
Исправит ли ситуацию использование JPA? Не всегда. В приведенном ниже варианте использование JPA ничего не меняет.
public List<Order> findOrdersByName(String enteredName) {
return entityManager.createQuery("select * from orders “
+ ”where name like '" + enteredName + "'";
Order.class)
.getResultList());
}
Как сделать лучше?
1. Использовать параметризованные запросы
Достаточно добавить минимальные изменения в прошлый пример с JPA, которые помогут безопасно вставлять введенные пользователем значения в запрос перед его выполнением:
public List<Order> findOrdersByName(String enteredName) {
return entityManager.createQuery("select * from orders “
+ ”where name like ':enteredName'";
Order.class)
.setParameter("enteredName", enteredName)
.getResultList());
}
Мы добавили параметр :enteredName в запрос и позволили EntityManager’у самому безопасно подставить введенную пользователем строку: .setParameter("enteredName", enteredName). Только и всего :)
Также можно изменить и первый пример и использовать параметры, вместо конкатенации строк втупую. Уберем конкатенацию и заменим ее на параметр “?”. И потом установим параметр при помощи p.setString(1, enteredName):
public List<Orders> findOrdersByName(String enteredName) throws SQLException {
String sql = "select * from orders "
+ " where name like ?";
Connection c = dataSource.getConnection();
PreparedStatement p = c.prepareStatement(sql);
p.setString(1, enteredName);
return p.executeQuery(sql));
}
Минус этого метода в том, что строить динамические запросы становится затруднительно: придется строить сложную логику для управления параметрами, особенно, если их много и у них сложная логика установки.
2. Использование JPA Criteria API
Criteria API - это обращение к базе на более высоком уровне абстракции. Здесь не приходится напрямую использовать SQL-запросы, но нужно собирать их по кусочкам. Для организации работы с базой приходится использовать больше строк кода, но при помощи Criteria API безопасные динамические запросы генерировать становится проще.
public List<Orders> findOrdersByName(String enteredName) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> root = cq.from(Order.class);
cq.select(root).where(cb.equal(root.get(Order_.name), enteredName));
return entityManager.createQuery(q).getResultList();
}
3. Хранимые процедуры
Хранимые процедуры не всегда защищены от SQL-инъекций. Однако некоторые стандартные конструкции программирования хранимых процедур имеют тот же эффект, что и использование параметризованных запросов.
В следующем примере кода используется CallableStatement, реализация интерфейса хранимой процедуры в Java, для выполнения того же запроса к базе данных. Хранимая процедура sp_getAccountBalance должна быть предварительно определена в базе данных.
public List<Orders> findOrdersByName(String enteredName) {
CallableStatement cs = connection.prepareCall("{call sp_getOrdersByName(?)}");
cs.setString(1, enteredName);
return cs.executeQuery().getResultList();
}
4. Обработка пользовательских данных
Здесь есть 2 общепринятых способа:
- Экранирование введенных пользователем данных
- Проверка введенных данных в запросах
Эти методы следует использовать только в крайнем случае, когда ни один из вышеперечисленных способов невозможен. Эффективность и отказоустойчивость приведенных методов будет напрямую зависеть от алгоритма и невозможно гарантировать, что она предотвратит все SQL-инъекции во всех ситуациях.