← Назад к вопросам

Что нужно делать, чтобы не допустить возможность SQL Injection?

2.2 Middle🔥 231 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ: Как предотвратить SQL Injection

SQL Injection — одна из самых опасных уязвимостей в веб-приложениях. Это атака, при которой злоумышленник внедряет вредоносный SQL код в пользовательский ввод. Вот полный спектр мер защиты.

Пример SQL Injection атаки

// ОПАСНО! Уязвимый код
String username = request.getParameter("username");  // Вводит: admin' OR '1'='1
String password = request.getParameter("password");

String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
// Результирующий запрос:
// SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'anything'
// Условие '1'='1' ВСЕГДА истинно! Вернёт все пользователей.

Защита #1: Prepared Statements (ОСНОВНОЙ МЕТОД)

Это абсолютная защита от SQL Injection — используй параметризованные запросы.

// ПРАВИЛЬНО! Защищено от injection
String username = request.getParameter("username");
String password = request.getParameter("password");

String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = connection.prepareStatement(query);

stmt.setString(1, username);  // Параметры экранируются автоматически
stmt.setString(2, password);

ResultSet rs = stmt.executeQuery();

Как работает: PreparedStatement отделяет SQL структуру от данных. Данные передаются отдельно и автоматически экранируются.

ОПАСНО:     query = "SELECT * FROM users WHERE id = '" + userId + "'"
БЕЗОПАСНО:  query = "SELECT * FROM users WHERE id = ?"
            stmt.setLong(1, userId)  // Данные отделены от SQL

Защита #2: ORM Frameworks (Hibernate, JPA)

ОРМ фреймворки используют Prepared Statements под капотом.

// Hibernate JPQL (безопасно)
Session session = sessionFactory.openSession();
Query query = session.createQuery("FROM User WHERE username = :username");
query.setParameter("username", username);  // Параметр безопасен
List<User> users = query.list();
// Spring Data JPA (безопасно)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.username = :username")
    User findByUsername(@Param("username") String username);
}

// Использование
User user = userRepository.findByUsername(username);  // Защищено

НЕ БЕЗОПАСНО в ORM:

// ОПАСНО! Используешь String конкатенацию
@Query("SELECT u FROM User u WHERE u.username = '" + username + "'")
User findByUsername(String username);

Защита #3: Валидация и Санитизация входных данных

Проверяй входные данные ДО использования в запросах.

public User login(String username, String password) {
    // 1. Валидация формата
    if (username == null || username.isEmpty()) {
        throw new IllegalArgumentException("Username cannot be empty");
    }
    
    if (!username.matches("^[a-zA-Z0-9_]{3,20}$")) {  // Только буквы, цифры, подчеркивание
        throw new IllegalArgumentException("Invalid username format");
    }
    
    if (password.length() < 8) {
        throw new IllegalArgumentException("Invalid password");
    }
    
    // 2. Максимальная длина
    if (username.length() > 50) {
        throw new IllegalArgumentException("Username too long");
    }
    
    // 3. Использование в безопасном запросе
    return userRepository.findByUsername(username);
}

Защита #4: Наименьших привилегий (Principle of Least Privilege)

Делай учётные записи БД с минимальными правами.

-- ОПАСНО! Один пользователь со всеми правами
CREATE USER 'app'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'app'@'localhost';

-- ПРАВИЛЬНО! Разные пользователи для разных операций
CREATE USER 'app_read'@'localhost' IDENTIFIED BY 'pass1';
GRANT SELECT ON mydb.* TO 'app_read'@'localhost';

CREATE USER 'app_write'@'localhost' IDENTIFIED BY 'pass2';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app_write'@'localhost';

CREATE USER 'app_admin'@'localhost' IDENTIFIED BY 'pass3';
GRANT ALL PRIVILEGES ON mydb.* TO 'app_admin'@'localhost';

// Java приложение использует разные соединения
public class DatabaseConfig {
    public DataSource readDataSource() {
        return DriverManagerDataSource("...", "app_read", "pass1");
    }
    
    public DataSource writeDataSource() {
        return DriverManagerDataSource("...", "app_write", "pass2");
    }
}

Если хакер скомпрометирует приложение, он получит только те права, которые нужны для чтения данных.

Защита #5: Экранирование специальных символов

Если по какой-то причине НЕ можешь использовать Prepared Statements:

import org.owasp.esapi.ESAPI;
import org.apache.commons.lang3.StringEscapeUtils;

public String escapeSqlString(String input) {
    // Способ 1: Apache Commons
    return StringEscapeUtils.escapeSql(input);
    
    // Способ 2: OWASP ESAPI
    // return ESAPI.encoder().encodeForSQL(input);
}

// ПЛОХО! Даже с экранированием
String escaped = escapeSqlString(username);
String query = "SELECT * FROM users WHERE username = '" + escaped + "'";
// Не достаточно защиты!

НИКОГДА не полагайся только на экранирование! Используй Prepared Statements.

Защита #6: Web Application Firewall (WAF)

Добавь слой защиты на уровне сервера.

Пользователь
    ↓
[WAF - ModSecurity/F5]
    ↓
Приложение
    ↓
База данных

WAF анализирует запросы и блокирует типичные SQL Injection паттерны.

Защита #7: Логирование и Мониторинг

Ослеживай подозрительные запросы.

import org.slf4j.Logger;

@Service
public class AuthService {
    private static final Logger log = LoggerFactory.getLogger(AuthService.class);
    
    public User login(String username, String password) {
        log.info("Login attempt: username={}", username);
        
        try {
            User user = userRepository.findByUsername(username);
            
            if (user == null) {
                log.warn("Failed login: user not found - {}", username);
                return null;
            }
            
            return user;
        } catch (Exception e) {
            log.error("SQL Exception during login: {}", e.getMessage(), e);
            // Не выводи детали ошибок пользователю!
            throw new RuntimeException("Database error");
        }
    }
}

Что логировать:

  • Все попытки входа (успешные и неудачные)
  • Необычные SQL исключения
  • Много ошибок от одного IP

Защита #8: Обновления и Патчи

Держи библиотеки в актуальном состоянии.

<!-- pom.xml -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>  <!-- Актуальная версия -->
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.2.0</version>  <!-- Свежая версия -->
</dependency>

Полная защищённая реализация

@Service
public class SecureUserService {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional(readOnly = true)
    public User findUser(String username, String password) {
        // 1. Валидация
        if (!isValidUsername(username)) {
            throw new IllegalArgumentException("Invalid username");
        }
        
        // 2. Использование ORM с параметрами
        User user = userRepository.findByUsername(username);
        
        if (user == null) {
            log.warn("User not found: {}", username);
            return null;
        }
        
        // 3. Проверка пароля (никогда не сравнивай строки!)
        if (!passwordEncoder.matches(password, user.getPasswordHash())) {
            log.warn("Invalid password for user: {}", username);
            return null;
        }
        
        return user;
    }
    
    private boolean isValidUsername(String username) {
        return username != null
            && username.matches("^[a-zA-Z0-9_]{3,50}$")
            && !username.equalsIgnoreCase("admin");
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // Parametrized query - безопасно!
    @Query("SELECT u FROM User u WHERE u.username = :username")
    Optional<User> findByUsername(@Param("username") String username);
}

Чеклист защиты от SQL Injection

✅ Используй Prepared Statements / Parametrized Queries ✅ Используй ORM frameworks (Hibernate, JPA) ✅ Валидируй входные данные ✅ Применяй Principle of Least Privilege к БД ✅ Логируй подозрительные запросы ✅ Обновляй зависимости ✅ Используй HTTPS для передачи данных ✅ Не выводи детали SQL ошибок пользователю ✅ Используй WAF на production ✅ Проводи Security Reviews кода

❌ Никогда не конкатенируй пользовательский ввод в SQL ❌ Не полагайся только на экранирование ❌ Не отключай JDBC URL параметры безопасности ❌ Не сохраняй пароли в открытом виде

Итоговый вывод

Предотвращение SQL Injection требует многоуровневого подхода. Основная защита — использование Prepared Statements, но это должно быть дополнено валидацией, наименьшими привилегиями и мониторингом. В Java экосистеме ORM фреймворки обеспечивают хорошую защиту по умолчанию, но разработчик должен правильно их использовать.

Что нужно делать, чтобы не допустить возможность SQL Injection? | PrepBro