Skip to content
IRC-CodingIRC-Coding
SQL InjectionУязвимость безопасностиPrepared StatementsORM фреймворкиВалидация входных данных

SQL Injection: риски, защита и лучшие практики

SQL Injection угрозы и защита. Prepared Statements, параметризованные запросы, ORM фреймворки и валидация входных данных.

S

schutzgeist

15 min read
SQL Injection: риски, защита и лучшие практики

SQL Injection: уязвимость, методы защиты и best practices

В этом материале разбираем полный спектр SQL Injection — от механики атак до практических методов защиты, Prepared Statements, ORM фреймворков и валидации входных данных с рабочими примерами.

Суть

SQL Injection происходит, когда злоумышленник внедряет вредоносные SQL-команды через поля ввода. Защищаются Prepared Statements, валидацией входа и ORM фреймворками.

Определение

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

Векторы атак:

  • Формы авторизации: обход аутентификации
  • Поля поиска: извлечение конфиденциальных данных
  • URL-параметры: манипуляция запросами к базе
  • Загрузка файлов: внедрение через имена файлов
  • API-endpoints: прямое изменение данных в БД

Способы защиты:

  • Prepared Statements: параметризованные запросы
  • Валидация входа: проверка по whitelist
  • ORM Framework: абстракция для доступа к БД
  • Least Privilege: минимальные права на БД
  • Error Handling: скрытие ошибок БД от пользователя

Ключевые пункты для проверок

  • SQL Injection: внедрение команд через поля ввода
  • Цели атак: извлечение, манипуляция, удаление данных
  • Уязвимые точки: формы входа, поиск, параметры URL
  • Защита: Prepared Statements, валидация, ORM
  • Prepared Statements: параметризованные запросы с placeholders
  • ORM Framework: Hibernate, Entity Framework, SQLAlchemy
  • Least Privilege: минимальные права доступа к БД
  • Релевантно для IHK: критический риск в веб-приложениях

Основные компоненты

  1. Векторы атак: возможные точки внедрения
  2. Техники injection: Union-based, Boolean-based, Time-based
  3. Методы защиты: Prepared Statements, валидация, ORM
  4. ORM Framework: слой абстракции для работы с БД
  5. Валидация входа: проверка данных по whitelist
  6. Обработка ошибок: безопасное логирование
  7. Security Headers: дополнительные слои защиты
  8. Мониторинг: обнаружение попыток атак

Примеры из практики

1. Атаки SQL Injection и противодействие

import java.sql.*;
import java.util.Scanner;
import java.util.regex.Pattern;

// Уязвимая реализация (подвержена SQL Injection)
class UnsafeLoginService {
    private Connection connection;
    
    public UnsafeLoginService(Connection connection) {
        this.connection = connection;
    }
    
    public boolean loginUnsafe(String username, String password) {
        // ОЧЕНЬ ОПАСНО - прямое конкатенирование строк
        String query = "SELECT * FROM users WHERE username = '" + username + 
                      "' AND password = '" + password + "'";
        
        System.out.println("Query: " + query);
        
        try (Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery(query)) {
            
            return rs.next(); // Успешный вход, если есть результат
            
        } catch (SQLException e) {
            System.err.println("Datenbankfehler: " + e.getMessage());
            return false;
        }
    }
}

// Безопасная реализация с Prepared Statements
class SafeLoginService {
    private Connection connection;
    
    public SafeLoginService(Connection connection) {
        this.connection = connection;
    }
    
    public boolean loginSafe(String username, String password) {
        // БЕЗОПАСНО - Prepared Statement с placeholders
        String query = "SELECT * FROM users WHERE username = ? AND password = ?";
        
        try (PreparedStatement pstmt = connection.prepareStatement(query)) {
            
            // Установка параметров (автоматический escape)
            pstmt.setString(1, username);
            pstmt.setString(2, password);
            
            try (ResultSet rs = pstmt.executeQuery()) {
                return rs.next();
            }
            
        } catch (SQLException e) {
            System.err.println("Datenbankfehler: " + e.getMessage());
            return false;
        }
    }
    
    // С валидацией входа
    public boolean loginWithValidation(String username, String password) {
        // Проверка входных данных перед запросом
        if (!isValidUsername(username) || !isValidPassword(password)) {
            System.err.println("Ungültige Eingabe");
            return false;
        }
        
        return loginSafe(username, password);
    }
    
    private boolean isValidUsername(String username) {
        // Whitelist: только буквы, цифры и underscore
        Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]{3,20}$");
        return pattern.matcher(username).matches();
    }
    
    private boolean isValidPassword(String password) {
        // Минимум 8 символов, максимум 100
        return password != null && 
               password.length() >= 8 && 
               password.length() <= 100;
    }
}

// Расширенные меры безопасности
class AdvancedSecurityService {
    private Connection connection;
    
    public AdvancedSecurityService(Connection connection) {
        this.connection = connection;
    }
    
    // С Rate Limiting
    private Map<String, Integer> loginAttempts = new java.util.concurrent.ConcurrentHashMap<>();
    private static final int MAX_ATTEMPTS = 5;
    private static final long LOCK_TIME_MS = 30000; // 30 минут
    
    public boolean loginWithRateLimit(String username, String password) {
        String clientIp = getClientIp(); // Требуется реализация
        
        // Проверка Rate Limiting
        if (isBlocked(clientIp)) {
            System.err.println("IP заблокирован из-за множественных попыток");
            return false;
        }
        
        boolean loginSuccess = loginSafe(username, password);
        
        if (!loginSuccess) {
            incrementAttempts(clientIp);
        } else {
            clearAttempts(clientIp);
        }
        
        return loginSuccess;
    }
    
    private boolean isBlocked(String clientIp) {
        Integer attempts = loginAttempts.get(clientIp);
        return attempts != null && attempts >= MAX_ATTEMPTS;
    }
    
    private void incrementAttempts(String clientIp) {
        loginAttempts.merge(clientIp, 1, Integer::sum);
    }
    
    private void clearAttempts(String clientIp) {
        loginAttempts.remove(clientIp);
    }
    
    private String getClientIp() {
        // Реализация получения IP
        return "127.0.0.1"; // Placeholder
    }
    
    // С логированием и мониторингом
    public boolean loginWithMonitoring(String username, String password) {
        long startTime = System.currentTimeMillis();
        String clientIp = getClientIp();
        
        try {
            boolean success = loginSafe(username, password);
            
            // Логирование попытки входа
            logLoginAttempt(username, clientIp, success, 
                          System.currentTimeMillis() - startTime);
            
            return success;
            
        } catch (Exception e) {
            logSecurityEvent("Login-Fehler", username, clientIp, e.getMessage());
            return false;
        }
    }
    
    private void logLoginAttempt(String username, String clientIp, 
                               boolean success, long duration) {
        String logLevel = success ? "INFO" : "WARN";
        System.out.printf("[%s] Login %s: user=%s, ip=%s, duration=%dms%n",
                         logLevel, success ? "erfolgreich" : "fehlgeschlagen",
                         username, clientIp, duration);
    }
    
    private void logSecurityEvent(String event, String username, 
                                 String clientIp, String details) {
        System.out.printf("[SECURITY] %s: user=%s, ip=%s, details=%s%n",
                         event, username, clientIp, details);
    }
}

// Демонстрация SQL Injection
public class SQLInjectionDemo {
    
    public static void main(String[] args) {
        System.out.println("=== SQL Injection Demo ===");
        
        try {
            // Подключение к H2 in-memory БД
            Connection conn = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");
            
            // Создание тестовой БД
            setupTestDatabase(conn);
            
            // Создание сервисов
            UnsafeLoginService unsafeService = new UnsafeLoginService(conn);
            SafeLoginService safeService = new SafeLoginService(conn);
            AdvancedSecurityService advancedService = new AdvancedSecurityService(conn);
            
            // Обычные попытки входа
            System.out.println("\n--- Обычные попытки входа ---");
            testLogin(unsafeService, "admin", "password123", "Unsicher");
            testLogin(safeService, "admin", "password123", "Sicher");
            
            // Атаки SQL Injection
            System.out.println("\n--- Атаки SQL Injection ---");
            
            // Классическая атака injection
            String injectionUsername = "admin' OR '1'='1";
            String injectionPassword = "anything";
            
            System.out.println("Injection-Username: " + injectionUsername);
            System.out.println("Injection-Password: " + injectionPassword);
            
            boolean unsafeResult = unsafeService.loginUnsafe(injectionUsername, injectionPassword);
            System.out.println("Unsicherer Login (Injection): " + 
                             (unsafeResult ? "ERFOLGREICH (Gefahr!)" : "fehlgeschlagen"));
            
            boolean safeResult = safeService.loginSafe(injectionUsername, injectionPassword);
            System.out.println("Sicherer Login (Injection): " + 
                             (safeResult ? "erfolgreich" : "fehlgeschlagen"));
            
            // Расширенные меры защиты
            System.out.println("\n--- Расширенные меры защиты ---");
            
            // С валидацией входа
            boolean validationResult = safeService.loginWithValidation("admin", "password123");
            System.out.println("Login с валидацией: " + 
                             (validationResult ? "erfolgreich" : "fehlgeschlagen"));
            
            boolean invalidResult = safeService.loginWithValidation("admin'; DROP TABLE users; --", "password");
            System.out.println("Login с невалидными данными: " + 
                             (invalidResult ? "erfolgreich" : "fehlgeschlagen"));
            
            // С Rate Limiting
            boolean rateLimitedResult = advancedService.loginWithRateLimit("admin", "password123");
            System.out.println("Login с Rate Limiting: " + 
                             (rateLimitedResult ? "erfolgreich" : "fehlgeschlagen"));
            
            // С мониторингом
            boolean monitoredResult = advancedService.loginWithMonitoring("admin", "password123");
            System.out.println("Login с мониторингом: " + 
                             (monitoredResult ? "erfolgreich" : "fehlgeschlagen"));
            
            conn.close();
            
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    
    private static void testLogin(Object service, String username, String password, String type) {
        try {
            boolean result;
            if (service instanceof UnsafeLoginService) {
                result = ((UnsafeLoginService) service).loginUnsafe(username, password);
            } else if (service instanceof SafeLoginService) {
                result = ((SafeLoginService) service).loginSafe(username, password);
            } else {
                result = false;
            }
            
            System.out.printf("%s Login (%s, %s): %s%n", 
                             type, username, password, 
                             result ? "erfolgreich" : "fehlgeschlagen");
        } catch (Exception e) {
            System.out.printf("%s Login: Fehler - %s%n", type, e.getMessage());
        }
    }
    
    private static void setupTestDatabase(Connection conn) throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // Создание таблицы users
            stmt.execute("DROP TABLE users IF EXISTS");
            stmt.execute("""
                CREATE TABLE users (
                    id INT PRIMARY KEY AUTO_INCREMENT,
                    username VARCHAR(50) UNIQUE NOT NULL,
                    password VARCHAR(100) NOT NULL,
                    email VARCHAR(100),
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """);
            
            // Добавление тестовых данных
            stmt.execute("INSERT INTO users (username, password, email) VALUES " +
                         "('admin', 'password123', 'admin@example.com'), " +
                         "('user1', 'user123', 'user1@example.com'), " +
                         "('user2', 'user456', 'user2@example.com')");
            
            System.out.println("Test-Datenbank erstellt");
        }
    }
}

2. Защита в ORM фреймворках

import javax.persistence.*;
import java.util.List;
import java.util.regex.Pattern;

// JPA Entity
@Entity
@Table(name = "users")
class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    private String email;
    
    // Getter и Setter
    public Long getId() { return id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

// Безопасный Service с JPA
class SecureUserService {
    private EntityManager em;
    
    public SecureUserService(EntityManager em) {
        this.em = em;
    }
    
    // Безопасная проверка входа через JPA
    public User authenticate(String username, String password) {
        // Валидация входных данных
        if (!isValidInput(username) || !isValidInput(password)) {
            throw new IllegalArgumentException("Ungültige Eingabe");
        }
        
        try {
            // JPA Query с именованными параметрами (защита встроена)
            TypedQuery<User> query = em.createQuery(
                "SELECT u FROM User u WHERE u.username = :username AND u.password = :password", 
                User.class);
            
            query.setParameter("username", username);
            query.setParameter("password", password);
            
            List<User> results = query.getResultList();
            
            return results.isEmpty() ? null : results.get(0);
            
        } catch (Exception e) {
            // Безопасная обработка ошибок
            throw new RuntimeException("Authentifizierung fehlgeschlagen", e);
        }
    }
    
    // Безопасный поиск через Criteria API
    public List<User> searchUsers(String searchTerm) {
        if (!isValidSearchTerm(searchTerm)) {
            throw new IllegalArgumentException("Ungültiger Suchbegriff");
        }
        
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<User> cq = cb.createQuery(User.class);
        Root<User> user = cq.from(User.class);
        
        // Безопасный поиск через Criteria API (без конкатенации строк)
        Predicate searchPredicate = cb.or(
            cb.like(user.get("username"), "%" + searchTerm + "%"),
            cb.like(user.get("email"), "%" + searchTerm + "%")
        );
        
        cq.where(searchPredicate);
        
        return em.createQuery(cq).getResultList();
    }
    
    // Безопасная пагинация
    public List<User> getUsersPaginated(int page, int size) {
        if (page < 0 || size <= 0 || size > 100) {
            throw new IllegalArgumentException("Ungültige Paginierungsparameter");
        }
        
        TypedQuery<User> query = em.createQuery("SELECT u FROM User u ORDER BY u.username", User.class);
        query.setFirstResult(page * size);
        query.setMaxResults(size);
        
        return query.getResultList();
    }
    
    private boolean isValidInput(String input) {
        // Валидация по whitelist
        Pattern pattern = Pattern.compile("^[a-zA-Z0-9_@.-]{3,100}$");
        return input != null && pattern.matcher(input).matches();
    }
    
    private boolean isValidSearchTerm(String term) {
        // Поисковые термины допускают больше символов
        Pattern pattern = Pattern.compile("^[a-zA-Z0-9_@.-\\s]{1,50}$");
        return term != null && pattern.matcher(term).matches();
    }
}

// Расширенная защита со Spring Data JPA
@Repository
interface UserRepository extends JpaRepository<User, Long> {
    
    // Безопасные запросы через методы
    Optional<User> findByUsernameAndPassword(String username, String password);
    
    @Query("SELECT u FROM User u WHERE u.username LIKE %:search% OR u.email LIKE %:search%")
    List<User> findByUsernameOrEmailContaining(@Param("search") String search);
    
    // Native Query с привязкой параметров
    @Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
    Optional<User> findByEmailNative(@Param("email") String email);
}

// Service со Spring Data JPA
@Service
@Transactional
class UserServiceWithSpring {
    private UserRepository userRepository;
    
    public UserServiceWithSpring(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public Optional<User> authenticate(String username, String password) {
        // Валидация входных данных
        validateInput(username, "username");
        validateInput(password, "password");
        
        // Spring Data JPA защищает автоматически
        return userRepository.findByUsernameAndPassword(username, password);
    }
    
    public List<User> searchUsers(String searchTerm) {
        validateSearchTerm(searchTerm);
        return userRepository.findByUsernameOrEmailContaining(searchTerm);
    }
    
    private void validateInput(String input, String fieldName) {
        if (input == null || input.trim().isEmpty()) {
            throw new IllegalArgumentException(fieldName + " darf nicht leer sein");
        }
        
        if (input.length() > 100) {
            throw new IllegalArgumentException(fieldName + " ist zu lang");
        }
        
        // Weitere Validierungsregeln...
    }
    
    private void validateSearchTerm(String term) {
        if (term == null || term.trim().isEmpty()) {
            throw new IllegalArgumentException("Suchbegriff darf nicht leer sein");
        }
        
        if (term.length() > 50) {
            throw new IllegalArgumentException("Suchbegriff ist zu lang");
        }
    }
}

// ORM демонстрация
public class ORMSecurityDemo {
    
    public static void main(String[] args) {
        System.out.println("=== ORM Security Demo ===");
        
        // В реальном приложении это управляется через Spring/DI
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("testdb");
        EntityManager em = emf.createEntityManager();
        
        try {
            // Создание тестовых данных
            setupTestData(em);
            
            // Создание Service
            SecureUserService service = new SecureUserService(em);
            
            // Нормальная аутентификация
            System.out.println("\n--- Нормальная аутентификация ---");
            User user = service.authenticate("admin", "password123");
            System.out.println("Authentifizierung: " + (user != null ? "erfolgreich" : "fehlgeschlagen"));
            
            // Попытка Injection (будет заблокирована ORM)
            System.out.println("\n--- Попытка Injection ---");
            try {
                User injectedUser = service.authenticate("admin' OR '1'='1", "anything");
                System.out.println("Injection-Authentifizierung: " + 
                                 (injectedUser != null ? "erfolgreich (Gefahr!)" : "fehlgeschlagen"));
            } catch (Exception e) {
                System.out.println("Injection-Authentifizierung: fehlgeschlagen (geschützt)");
            }
            
            // Безопасный поиск
            System.out.println("\n--- Безопасный поиск ---");
            List<User> searchResults = service.searchUsers("admin");
            System.out.println("Suchergebnisse: " + searchResults.size() + " Benutzer");
            
            // Пагинация
            System.out.println("\n--- Пагинация ---");
            List<User> paginatedUsers = service.getUsersPaginated(0, 2);
            System.out.println("Paginierte Ergebnisse: " + paginatedUsers.size() + " Benutzer");
            
        } finally {
            em.close();
            emf.close();
        }
    }
    
    private static void setupTestData(EntityManager em) {
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            
            // Создание тестовых пользователей
            User admin = new User();
            admin.setUsername("admin");
            admin.setPassword("password123");
            admin.setEmail("admin@example.com");
            em.persist(admin);
            
            User user1 = new User();
            user1.setUsername("user1");
            user1.setPassword("user123");
            user1.setEmail("user1@example.com");
            em.persist(user1);
            
            tx.commit();
            System.out.println("Test-Daten erstellt");
            
        } catch (Exception e) {
            if (tx.isActive()) {
                tx.rollback();
            }
            e.printStackTrace();
        }
    }
}

3. Примеры на PHP и Python

<?php
// Уязвимая реализация PHP (небезопасная)
class UnsafeLoginPHP {
    private $pdo;
    
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    
    public function loginUnsafe($username, $password) {
        // ОПАСНО - Прямая конкатенация строк
        $query = "SELECT * FROM users WHERE username = '" . $username . 
                "' AND password = '" . $password . "'";
        
        echo "Query: " . $query . "\n";
        
        $stmt = $this->pdo->query($query);
        return $stmt->rowCount() > 0;
    }
}

// Защищённая реализация PHP
class SafeLoginPHP {
    private $pdo;
    
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    
    public function loginSafe($username, $password) {
        // БЕЗОПАСНО - Prepared Statement
        $query = "SELECT * FROM users WHERE username = ? AND password = ?";
        
        $stmt = $this->pdo->prepare($query);
        $stmt->execute([$username, $password]);
        
        return $stmt->rowCount() > 0;
    }
    
    public function loginWithValidation($username, $password) {
        // Валидация входных данных
        if (!$this->isValidUsername($username) || !$this->isValidPassword($password)) {
            throw new InvalidArgumentException("Ungültige Eingabe");
        }
        
        return $this->loginSafe($username, $password);
    }
    
    private function isValidUsername($username) {
        // Whitelist-валидация
        return preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username);
    }
    
    private function isValidPassword($password) {
        return strlen($password) >= 8 && strlen($password) <= 100;
    }
}

// Demo PHP
echo "=== PHP SQL Injection Demo ===\n";

try {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
    
    // Создание сервисов
    $unsafeService = new UnsafeLoginPHP($pdo);
    $safeService = new SafeLoginPHP($pdo);
    
    // Обычные попытки входа
    echo "\n--- Обычные попытки входа ---\n";
    $unsafeResult = $unsafeService->loginUnsafe('admin', 'password123');
    echo "Небезопасный вход: " . ($unsafeResult ? 'успешно' : 'ошибка') . "\n";
    
    $safeResult = $safeService->loginSafe('admin', 'password123');
    echo "Защищённый вход: " . ($safeResult ? 'успешно' : 'ошибка') . "\n";
    
    // Атака SQL Injection
    echo "\n--- Атака SQL Injection ---\n";
    $injectionUsername = "admin' OR '1'='1";
    $injectionPassword = "anything";
    
    echo "Username для инъекции: $injectionUsername\n";
    echo "Password для инъекции: $injectionPassword\n";
    
    $unsafeInjectionResult = $unsafeService->loginUnsafe($injectionUsername, $injectionPassword);
    echo "Небезопасный вход (инъекция): " . 
         ($unsafeInjectionResult ? 'УСПЕШНО (Опасно!)' : 'ошибка') . "\n";
    
    $safeInjectionResult = $safeService->loginSafe($injectionUsername, $injectionPassword);
    echo "Защищённый вход (инъекция): " . 
         ($safeInjectionResult ? 'успешно' : 'ошибка') . "\n";
    
} catch (PDOException $e) {
    echo "Ошибка БД: " . $e->getMessage() . "\n";
}
?>
# Python SQLAlchemy реализация
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import re

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    password = Column(String(100), nullable=False)
    email = Column(String(100))
    
    def __repr__(self):
        return f"<User(username='{self.username}')>"

class SecureUserService:
    def __init__(self, session):
        self.session = session
    
    def authenticate(self, username, password):
        # Валидация входных данных
        if not self.is_valid_input(username) or not self.is_valid_input(password):
            raise ValueError("Ungültige Eingabe")
        
        # SQLAlchemy Query - автоматически защищена
        user = self.session.query(User).filter(
            User.username == username,
            User.password == password
        ).first()
        
        return user
    
    def search_users(self, search_term):
        if not self.is_valid_search_term(search_term):
            raise ValueError("Ungültiger Suchbegriff")
        
        # Безопасный поиск с SQLAlchemy
        users = self.session.query(User).filter(
            (User.username.like(f"%{search_term}%")) |
            (User.email.like(f"%{search_term}%"))
        ).all()
        
        return users
    
    def is_valid_input(self, input_str):
        # Whitelist-валидация
        pattern = r'^[a-zA-Z0-9_@.-]{3,100}$'
        return input_str is not None and re.match(pattern, input_str) is not None
    
    def is_valid_search_term(self, term):
        # Поисковые термины с расширенным набором символов
        pattern = r'^[a-zA-Z0-9_@.-\s]{1,50}$'
        return term is not None and re.match(pattern, term) is not None

# Demo Python
def main():
    print("=== Python SQLAlchemy Security Demo ===")
    
    # Создание engine БД
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    
    Session = sessionmaker(bind=engine)
    session = Session()
    
    try:
        # Создание тестовых данных
        admin = User(username='admin', password='password123', email='admin@example.com')
        user1 = User(username='user1', password='user123', email='user1@example.com')
        
        session.add(admin)
        session.add(user1)
        session.commit()
        
        # Создание сервиса
        service = SecureUserService(session)
        
        # Обычная аутентификация
        print("\n--- Обычная аутентификация ---")
        user = service.authenticate('admin', 'password123')
        print(f"Аутентификация: {'успешно' if user else 'ошибка'}")
        
        # Попытка инъекции
        print("\n--- Попытка инъекции ---")
        try:
            injected_user = service.authenticate("admin' OR '1'='1", "anything")
            print(f"Аутентификация с инъекцией: {'успешно (Опасно!)' if injected_user else 'ошибка'}")
        except Exception as e:
            print(f"Аутентификация с инъекцией: ошибка (защищено)")
        
        # Безопасный поиск
        print("\n--- Безопасный поиск ---")
        search_results = service.search_users('admin')
        print(f"Результаты поиска: {len(search_results)} пользователей")
        
    finally:
        session.close()

if __name__ == "__main__":
    main()

Методы SQL Injection

Union-based Injection

-- Исходный запрос
SELECT * FROM users WHERE username = 'admin' AND password = 'password'

-- Инъекция
SELECT * FROM users WHERE username = 'admin' UNION SELECT table_name, null, null, null FROM information_schema.tables--' AND password = 'anything'

Boolean-based Injection

-- Исходный запрос
SELECT * FROM products WHERE category = 'electronics'

-- Инъекция
SELECT * FROM products WHERE category = 'electronics' AND 1=1--'  -- Всегда истина
SELECT * FROM products WHERE category = 'electronics' AND 1=0--'  -- Всегда ложь

Инъекция на основе временных задержек

-- Исходный запрос
SELECT * FROM users WHERE id = 1

-- Инъекция с временной задержкой
SELECT * FROM users WHERE id = 1 AND (SELECT SLEEP(5))--'  -- Задержка ответа при выполнении условия

Обзор защитных мер

МераОписаниеЭффективность
Prepared StatementsПараметризованные запросыОчень высокая
ORM FrameworkСлой абстракцииВысокая
Валидация входных данныхПроверка по белому спискуСредняя
Least PrivilegeМинимальные права на БДВысокая
Обработка ошибокСкрытие ошибок БД от клиентаСредняя
Rate LimitingОграничение количества попытокСредняя

Паттерны валидации входных данных

Проверка по белому списку

// Только разрешённые символы
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]{3,20}$");
boolean isValid = pattern.matcher(input).matches();

Проверка длины

// Ограничение по длине
if (input == null || input.length() < 3 || input.length() > 100) {
    throw new IllegalArgumentException("Недопустимая длина");
}

Проверка типа данных

// Проверка ожидаемых типов данных
try {
    int number = Integer.parseInt(input);
} catch (NumberFormatException e) {
    throw new IllegalArgumentException("Ожидается число");
}

Лучшие практики безопасности БД

Принцип минимальных привилегий

-- Пользователь веб-приложения с минимальными правами
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'secure_password';
GRANT SELECT, INSERT, UPDATE ON app_db.users TO 'webapp';
GRANT SELECT ON app_db.products TO 'webapp';
-- БЕЗ прав DROP, ALTER, CREATE

Хранимые процедуры

-- Безопасная хранимая процедура
CREATE PROCEDURE authenticate_user(IN p_username VARCHAR(50), IN p_password VARCHAR(100))
BEGIN
    SELECT id FROM users WHERE username = p_username AND password = p_password;
END;

-- Вызов из приложения
CALL authenticate_user(?, ?);

Мониторинг и обнаружение

Обнаружение аномалий

// Выявление подозрительных паттернов запросов
public class QueryMonitor {
    private Map<String, Integer> queryPatterns = new ConcurrentHashMap<>();
    
    public void monitorQuery(String query, String ip) {
        // Анализ паттерна запроса
        String pattern = extractPattern(query);
        
        // Проверка частоты
        int count = queryPatterns.merge(pattern, 1, Integer::sum);
        
        if (count > 100) { // Порог срабатывания
            alertSecurityTeam("Suspicious query pattern", ip, pattern);
        }
    }
    
    private String extractPattern(String query) {
        // Нормализация запроса для распознавания паттерна
        return query.replaceAll("\\d+", "N")
                  .replaceAll("'[^']*'", "'S'")
                  .toLowerCase();
    }
    
    private void alertSecurityTeam(String message, String ip, String details) {
        System.out.printf("[ALERT] %s from %s: %s%n", message, ip, details);
        // Send to SIEM system
    }
}

Преимущества и недостатки

Преимущества защитных мер

  • Безопасность: предотвращение компрометации БД
  • Соответствие стандартам: выполнение требований безопасности
  • Доверие: защита данных пользователей
  • Стабильность: предотвращение повреждения данных

Недостатки

  • Производительность: небольшой оверхед валидации
  • Сложность: требует дополнительного кода
  • Поддержка: необходимо регулярно обновлять
  • Ложные срабатывания: возможны ложные положительные результаты

Типовые вопросы на собеседованиях

  1. Что такое SQL Injection и как это работает? Внедрение SQL-команд через поля ввода путём конкатенации строк в запросы.

  2. Как лучше всего защититься от SQL Injection? Prepared Statements с параметризацией являются самым эффективным способом защиты.

  3. Почему ORM Framework безопаснее? Они абстрагируют SQL-запросы и автоматически используют Prepared Statements.

  4. В чём разница между валидацией по белому и чёрному списку? Белый список разрешает только определённые символы, чёрный список блокирует известные опасные паттерны.

Основные источники

  1. https://owasp.org/www-community/attacks/SQL_Injection
  2. https://www.w3schools.com/sql/sql_injection.asp
  3. https://portswigger.net/web-security/sql-injection
Назад к блогу
Share:

Похожие статьи