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

Что такое SQL injection и как защититься от него в Java?

1.0 Junior🔥 221 комментариев
#Базы данных и SQL

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

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

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

SQL Injection и защита от неё в Java

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

Что такое SQL Injection?

SQL Injection происходит, когда приложение конкатенирует пользовательский ввод напрямую в SQL запрос без должной санитизации или параметризации.

Пример уязвимого кода:

String username = userInput; // например: "admin' --"
String password = userInput; // например: "anything"

String sql = "SELECT * FROM users WHERE username = '" + username + 
             "' AND password = '" + password + "'";
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery(sql);

Если пользователь введёт username = "admin' --", итоговый SQL будет:

SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'

Комментарий -- скроет проверку пароля, и злоумышленник войдёт как admin!

Примеры атак

1. Аутентификация обхода (Login bypass):

username: admin' --
password: anything

Результат SQL:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
Пароль не проверяется!

2. Утечка данных (Data exfiltration):

username: ' OR '1'='1
password: ' OR '1'='1

Результат SQL:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '' OR '1'='1'
Вернёт ВСЕ пользователей!

3. Удаление данных (Data destruction):

username: admin'; DROP TABLE users; --
password: anything

Результат SQL:
SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --' AND password = 'anything'
Таблица users будет удалена!

Защита 1: Подготовленные операторы (Prepared Statements)

ЭТО ГЛАВНЫЙ СПОСОБ ЗАЩИТЫ!

Подготовленные операторы разделяют SQL код от данных. Параметры передаются отдельно и не интерпретируются как SQL код.

public class SafeDatabaseAccess {
    
    /**
     * ПРАВИЛЬНО: Использование PreparedStatement
     */
    public User authenticateUser(String username, String password) throws SQLException {
        String sql = "SELECT id, username, email FROM users WHERE username = ? AND password = ?";
        
        try (PreparedStatement preparedStatement = 
                connection.prepareStatement(sql)) {
            
            // Параметры передаются безопасно, не как SQL код
            preparedStatement.setString(1, username);
            preparedStatement.setString(2, password);
            
            ResultSet resultSet = preparedStatement.executeQuery();
            
            if (resultSet.next()) {
                return new User(
                    resultSet.getLong("id"),
                    resultSet.getString("username"),
                    resultSet.getString("email")
                );
            }
        }
        return null;
    }
    
    /**
     * НЕПРАВИЛЬНО: Конкатенация строк (УЯЗВИМО!)
     */
    public User authenticateUserUnsafe(String username, String password) 
            throws SQLException {
        String sql = "SELECT id, username, email FROM users WHERE username = '" + 
                     username + "' AND password = '" + password + "'";
        
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);
        
        if (resultSet.next()) {
            return new User(
                resultSet.getLong("id"),
                resultSet.getString("username"),
                resultSet.getString("email")
            );
        }
        return null;
    }
}

Защита 2: ORM (Object-Relational Mapping)

ORM фреймворки как Hibernate и JPA автоматически используют параметризованные запросы.

// Hibernate / JPA
@Repository
public class UserRepository extends JpaRepository<User, Long> {
    // БЕЗОПАСНО: JPA автоматически параметризует запрос
    User findByUsernameAndPassword(String username, String password);
}

// Использование
User user = userRepository.findByUsernameAndPassword("admin", "password123");

JPQL с параметрами:

@Repository
public class UserRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    // ПРАВИЛЬНО: Параметризованный JPQL запрос
    public User findByUsername(String username) {
        String jpql = "SELECT u FROM User u WHERE u.username = :username";
        
        return em.createQuery(jpql, User.class)
                 .setParameter("username", username)
                 .getResultList()
                 .stream()
                 .findFirst()
                 .orElse(null);
    }
}

НЕПРАВИЛЬНО (даже в JPQL):

// УЯЗВИМО!
String jpql = "SELECT u FROM User u WHERE u.username = '" + username + "'";
em.createQuery(jpql, User.class).getResultList();

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

Валидируйте на клиенте и сервере:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        // Валидация 1: Проверка наличия данных
        if (request.getUsername() == null || request.getPassword() == null) {
            throw new BadRequestException("Username and password required");
        }
        
        // Валидация 2: Проверка длины
        if (request.getUsername().length() > 50 || 
            request.getPassword().length() > 100) {
            throw new BadRequestException("Invalid input length");
        }
        
        // Валидация 3: Проверка формата (например, для email)
        String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
        if (!request.getUsername().matches(emailRegex)) {
            throw new BadRequestException("Invalid username format");
        }
        
        // Только после валидации вызываем безопасный метод с PreparedStatement
        return userService.authenticateUser(request.getUsername(), 
                                           request.getPassword());
    }
}

Защита 4: Минимальные привилегии БД

Создайте пользователя БД с минимальными правами:

-- Создание пользователя только для SELECT/INSERT/UPDATE
CREATE USER app_user@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON database.* TO app_user@'localhost';

-- БД пользователь НЕ имеет DROP/ALTER/DELETE
-- Таже если атакующий внедрит DROP, он не сработает

Защита 5: Использование хранимых процедур

С параметрами (безопасно):

public void callStoredProcedure(String username) throws SQLException {
    String sql = "{CALL authenticate_user(?)}";
    
    try (CallableStatement callableStatement = 
            connection.prepareCall(sql)) {
        
        callableStatement.setString(1, username);  // Параметр безопасен
        callableStatement.execute();
    }
}

Без параметров (уязвимо):

// НЕПРАВИЛЬНО!
String sql = "{CALL authenticate_user('" + username + "')}";
CallableStatement callableStatement = connection.prepareCall(sql);

Защита 6: Логирование и мониторинг

@Component
public class SqlInjectionDetector {
    
    private static final List<String> SUSPICIOUS_PATTERNS = Arrays.asList(
        "'", "\"", ";", "--", "/*", "*/", "xp_", "sp_", "DROP", "DELETE", 
        "UNION", "SELECT", "INSERT", "UPDATE", "EXEC"
    );
    
    public void validateInput(String input) {
        for (String pattern : SUSPICIOUS_PATTERNS) {
            if (input.toUpperCase().contains(pattern)) {
                logger.warn("Potential SQL injection attempt: {}", input);
                throw new SecurityException("Invalid input detected");
            }
        }
    }
}

Полный пример безопасного приложения

@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    /**
     * БЕЗОПАСНЫЙ метод аутентификации
     */
    @Transactional(readOnly = true)
    public Optional<User> authenticateUser(String username, String password) {
        // 1. Получение пользователя (JPA параметризует запрос)
        User user = userRepository.findByUsername(username);
        
        if (user == null) {
            return Optional.empty();
        }
        
        // 2. Сравнение хешированного пароля
        if (passwordEncoder.matches(password, user.getPasswordHash())) {
            return Optional.of(user);
        }
        
        return Optional.empty();
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // БЕЗОПАСНО: JPA параметризует запрос
    @Query("SELECT u FROM User u WHERE u.username = :username")
    User findByUsername(@Param("username") String username);
}

Сравнение подходов

ПодходБезопасноПримечание
Конкатенация строкУЯЗВИМО - НИКОГДА не используй
PreparedStatementЛучший вариант
JPA/HibernateORM автоматически параметризует
Валидация входа⚠️Дополнительная защита, не основная
Хранимые процедурыЕсли использовать параметры
Логирование⚠️Помогает обнаружить атаки

OWASP Top 10

SQL Injection занимает A03:2021 в OWASP Top 10 списке самых критичных уязвимостей веб-приложений.

Золотые правила

  1. НИКОГДА не конкатенируй пользовательский ввод в SQL
  2. ВСЕГДА используй PreparedStatement или ORM
  3. ВСЕГДА валидируй входные данные
  4. Используй параметризованные запросы везде — в Query, JPQL, SQL
  5. Логируй подозрительные попытки
  6. Давай БД пользователю минимальные права
  7. Обновляй зависимости регулярно

SQL Injection легко предотвратить, если использовать правильные инструменты и практики. Это ОБЯЗАТЕЛЬНОЕ знание для любого Java разработчика.

Что такое SQL injection и как защититься от него в Java? | PrepBro