Skip to content
IRC-CodingIRC-Coding
SQL InjectionRiesgo de SeguridadPrepared StatementsORM FrameworksValidación de Entrada

SQL Injection: Riesgos, Protección y Mejores Prácticas

Protege tus aplicaciones contra SQL Injection. Descubre Prepared Statements, validación de entrada y frameworks ORM para código seguro.

S

schutzgeist

16 min read
SQL Injection: Riesgos, Protección y Mejores Prácticas

SQL Injection: Riesgos de Seguridad, Medidas de Protección y Mejores Prácticas

Esta guía te ofrece un análisis completo de SQL Injection, incluyendo riesgos de seguridad, medidas de protección, Prepared Statements, frameworks ORM y validación de entrada con ejemplos prácticos.

Resumen Rápido

SQL Injection es una vulnerabilidad crítica que permite a los atacantes inyectar comandos SQL maliciosos. La protección se logra mediante Prepared Statements, validación de entrada y frameworks ORM.

Descripción Técnica Concisa

SQL Injection es una técnica de ataque donde los atacantes inyectan comandos SQL maliciosos a través de campos de entrada para obtener acceso no autorizado a bases de datos o manipular datos.

Vectores de ataque:

  • Formularios de login: Evasión de autenticación
  • Campos de búsqueda: Extracción de datos sensibles
  • Parámetros de URL: Manipulación de consultas a la base de datos
  • Cargas de archivos: Inyección a través de nombres de archivo
  • Endpoints de API: Manipulación directa de la base de datos

Medidas de protección:

  • Prepared Statements: Consultas parametrizadas
  • Validación de entrada: Verificación basada en listas blancas
  • Frameworks ORM: Capa de abstracción para acceso a bases de datos
  • Least Privilege: Derechos mínimos en la base de datos
  • Manejo de errores: Sin exposición de errores de base de datos

Puntos Clave para el Examen

  • SQL Injection: Inyección de comandos SQL a través de campos de entrada
  • Objetivos de ataque: Extracción, manipulación o destrucción de datos
  • Vulnerabilidades: Formularios de login, campos de búsqueda, parámetros de URL
  • Medidas de protección: Prepared Statements, validación de entrada, ORM
  • Prepared Statements: Consultas parametrizadas con marcadores de posición
  • Frameworks ORM: Hibernate, Entity Framework, SQLAlchemy
  • Least Privilege: Derechos mínimos para acceso a bases de datos
  • Relevancia profesional: Riesgo de seguridad crítico en aplicaciones web

Componentes Principales

  1. Vectores de ataque: Posibles puntos de entrada para inyección
  2. Técnicas de inyección: Basadas en Union, Boolean y Time
  3. Medidas de protección: Prepared Statements, validación, ORM
  4. Frameworks ORM: Capa de abstracción para acceso a bases de datos
  5. Validación de entrada: Verificación de entrada basada en listas blancas
  6. Manejo de errores: Gestión segura de excepciones
  7. Security Headers: Capas de protección adicionales
  8. Monitoreo: Detección de intentos de ataque

Ejemplos Prácticos

1. Ataques de SQL Injection y Contramedidas

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

// Implementación insegura (vulnerable a SQL Injection)
class UnsafeLoginService {
    private Connection connection;
    
    public UnsafeLoginService(Connection connection) {
        this.connection = connection;
    }
    
    public boolean loginUnsafe(String username, String password) {
        // MUY PELIGROSO - Concatenación directa de strings
        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(); // Login exitoso si hay resultado
            
        } catch (SQLException e) {
            System.err.println("Error de base de datos: " + e.getMessage());
            return false;
        }
    }
}

// Implementación segura con Prepared Statements
class SafeLoginService {
    private Connection connection;
    
    public SafeLoginService(Connection connection) {
        this.connection = connection;
    }
    
    public boolean loginSafe(String username, String password) {
        // SEGURO - Prepared Statement con marcadores de posición
        String query = "SELECT * FROM users WHERE username = ? AND password = ?";
        
        try (PreparedStatement pstmt = connection.prepareStatement(query)) {
            
            // Establecer parámetros (escapados automáticamente)
            pstmt.setString(1, username);
            pstmt.setString(2, password);
            
            try (ResultSet rs = pstmt.executeQuery()) {
                return rs.next();
            }
            
        } catch (SQLException e) {
            System.err.println("Error de base de datos: " + e.getMessage());
            return false;
        }
    }
    
    // Con validación de entrada
    public boolean loginWithValidation(String username, String password) {
        // Validar entrada antes de consultar la base de datos
        if (!isValidUsername(username) || !isValidPassword(password)) {
            System.err.println("Entrada inválida");
            return false;
        }
        
        return loginSafe(username, password);
    }
    
    private boolean isValidUsername(String username) {
        // Lista blanca: solo caracteres alfanuméricos y guión bajo
        Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]{3,20}$");
        return pattern.matcher(username).matches();
    }
    
    private boolean isValidPassword(String password) {
        // Mínimo 8 caracteres, máximo 100
        return password != null && 
               password.length() >= 8 && 
               password.length() <= 100;
    }
}

// Medidas de seguridad avanzadas
class AdvancedSecurityService {
    private Connection connection;
    
    public AdvancedSecurityService(Connection connection) {
        this.connection = connection;
    }
    
    // Con limitación de intentos
    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 minutos
    
    public boolean loginWithRateLimit(String username, String password) {
        String clientIp = getClientIp(); // Implementación requerida
        
        // Verificar limitación de intentos
        if (isBlocked(clientIp)) {
            System.err.println("IP bloqueada por demasiados intentos");
            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() {
        // Implementación para extraer la IP
        return "127.0.0.1"; // Marcador
    }
    
    // Con registro y monitoreo
    public boolean loginWithMonitoring(String username, String password) {
        long startTime = System.currentTimeMillis();
        String clientIp = getClientIp();
        
        try {
            boolean success = loginSafe(username, password);
            
            // Registrar el intento de login
            logLoginAttempt(username, clientIp, success, 
                          System.currentTimeMillis() - startTime);
            
            return success;
            
        } catch (Exception e) {
            logSecurityEvent("Error de login", 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 ? "exitoso" : "fallido",
                         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);
    }
}

// Demo de SQL Injection
public class SQLInjectionDemo {
    
    public static void main(String[] args) {
        System.out.println("=== Demo SQL Injection ===");
        
        try {
            // Conexión a base de datos en memoria H2
            Connection conn = DriverManager.getConnection("jdbc:h2:mem:testdb", "sa", "");
            
            // Crear tabla de prueba
            setupTestDatabase(conn);
            
            // Crear servicios
            UnsafeLoginService unsafeService = new UnsafeLoginService(conn);
            SafeLoginService safeService = new SafeLoginService(conn);
            AdvancedSecurityService advancedService = new AdvancedSecurityService(conn);
            
            // Intentos normales de login
            System.out.println("\n--- Intentos Normales de Login ---");
            testLogin(unsafeService, "admin", "password123", "Inseguro");
            testLogin(safeService, "admin", "password123", "Seguro");
            
            // Ataques de SQL Injection
            System.out.println("\n--- Ataques de SQL Injection ---");
            
            // Ataque de inyección clásico
            String injectionUsername = "admin' OR '1'='1";
            String injectionPassword = "anything";
            
            System.out.println("Username de inyección: " + injectionUsername);
            System.out.println("Password de inyección: " + injectionPassword);
            
            boolean unsafeResult = unsafeService.loginUnsafe(injectionUsername, injectionPassword);
            System.out.println("Login inseguro (inyección): " + 
                             (unsafeResult ? "EXITOSO (¡Peligro!)" : "fallido"));
            
            boolean safeResult = safeService.loginSafe(injectionUsername, injectionPassword);
            System.out.println("Login seguro (inyección): " + 
                             (safeResult ? "exitoso" : "fallido"));
            
            // Medidas de seguridad avanzadas
            System.out.println("\n--- Medidas de Seguridad Avanzadas ---");
            
            // Con validación de entrada
            boolean validationResult = safeService.loginWithValidation("admin", "password123");
            System.out.println("Login con validación: " + 
                             (validationResult ? "exitoso" : "fallido"));
            
            boolean invalidResult = safeService.loginWithValidation("admin'; DROP TABLE users; --", "password");
            System.out.println("Login con entrada inválida: " + 
                             (invalidResult ? "exitoso" : "fallido"));
            
            // Con limitación de intentos
            boolean rateLimitedResult = advancedService.loginWithRateLimit("admin", "password123");
            System.out.println("Login con limitación de intentos: " + 
                             (rateLimitedResult ? "exitoso" : "fallido"));
            
            // Con monitoreo
            boolean monitoredResult = advancedService.loginWithMonitoring("admin", "password123");
            System.out.println("Login con monitoreo: " + 
                             (monitoredResult ? "exitoso" : "fallido"));
            
            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 ? "exitoso" : "fallido");
        } catch (Exception e) {
            System.out.printf("%s Login: Error - %s%n", type, e.getMessage());
        }
    }
    
    private static void setupTestDatabase(Connection conn) throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // Crear tabla de usuarios
            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
                )
            """);
            
            // Insertar datos de prueba
            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("Base de datos de prueba creada");
        }
    }
}

2. Medidas de seguridad en 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;
    
    // Getters y setters
    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; }
}

// Clase service segura con JPA
class SecureUserService {
    private EntityManager em;
    
    public SecureUserService(EntityManager em) {
        this.em = em;
    }
    
    // Autenticación segura con JPA
    public User authenticate(String username, String password) {
        // Validación de entrada
        if (!isValidInput(username) || !isValidInput(password)) {
            throw new IllegalArgumentException("Entrada inválida");
        }
        
        try {
            // Query JPA con parámetros nombrados (segura automáticamente)
            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) {
            // Manejo seguro de errores
            throw new RuntimeException("Autenticación fallida", e);
        }
    }
    
    // Búsqueda segura con Criteria API de JPA
    public List<User> searchUsers(String searchTerm) {
        if (!isValidSearchTerm(searchTerm)) {
            throw new IllegalArgumentException("Término de búsqueda inválido");
        }
        
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<User> cq = cb.createQuery(User.class);
        Root<User> user = cq.from(User.class);
        
        // Búsqueda segura con Criteria API (sin concatenación de strings)
        Predicate searchPredicate = cb.or(
            cb.like(user.get("username"), "%" + searchTerm + "%"),
            cb.like(user.get("email"), "%" + searchTerm + "%")
        );
        
        cq.where(searchPredicate);
        
        return em.createQuery(cq).getResultList();
    }
    
    // Paginación segura
    public List<User> getUsersPaginated(int page, int size) {
        if (page < 0 || size <= 0 || size > 100) {
            throw new IllegalArgumentException("Parámetros de paginación inválidos");
        }
        
        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) {
        // Validación por whitelist
        Pattern pattern = Pattern.compile("^[a-zA-Z0-9_@.-]{3,100}$");
        return input != null && pattern.matcher(input).matches();
    }
    
    private boolean isValidSearchTerm(String term) {
        // Los términos de búsqueda permiten más caracteres
        Pattern pattern = Pattern.compile("^[a-zA-Z0-9_@.-\\s]{1,50}$");
        return term != null && pattern.matcher(term).matches();
    }
}

// Medidas avanzadas con Spring Data JPA
@Repository
interface UserRepository extends JpaRepository<User, Long> {
    
    // Consultas seguras con nombres de métodos
    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);
    
    // Query nativa con binding de parámetros
    @Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
    Optional<User> findByEmailNative(@Param("email") String email);
}

// Service con 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) {
        // Validación de entrada
        validateInput(username, "username");
        validateInput(password, "password");
        
        // Spring Data JPA, automáticamente segura
        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 + " no puede estar vacío");
        }
        
        if (input.length() > 100) {
            throw new IllegalArgumentException(fieldName + " es demasiado largo");
        }
        
        // Reglas de validación adicionales...
    }
    
    private void validateSearchTerm(String term) {
        if (term == null || term.trim().isEmpty()) {
            throw new IllegalArgumentException("El término de búsqueda no puede estar vacío");
        }
        
        if (term.length() > 50) {
            throw new IllegalArgumentException("El término de búsqueda es demasiado largo");
        }
    }
}

// Demo de seguridad en ORM
public class ORMSecurityDemo {
    
    public static void main(String[] args) {
        System.out.println("=== ORM Security Demo ===");
        
        // En una aplicación real, esto sería gestionado por Spring/DI
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("testdb");
        EntityManager em = emf.createEntityManager();
        
        try {
            // Crear datos de prueba
            setupTestData(em);
            
            // Crear el service
            SecureUserService service = new SecureUserService(em);
            
            // Autenticación normal
            System.out.println("\n--- Autenticación normal ---");
            User user = service.authenticate("admin", "password123");
            System.out.println("Autenticación: " + (user != null ? "exitosa" : "fallida"));
            
            // Intento de inyección (bloqueado por ORM)
            System.out.println("\n--- Intento de inyección ---");
            try {
                User injectedUser = service.authenticate("admin' OR '1'='1", "anything");
                System.out.println("Autenticación por inyección: " + 
                                 (injectedUser != null ? "exitosa (¡Peligro!)" : "fallida"));
            } catch (Exception e) {
                System.out.println("Autenticación por inyección: fallida (protegida)");
            }
            
            // Búsqueda segura
            System.out.println("\n--- Búsqueda segura ---");
            List<User> searchResults = service.searchUsers("admin");
            System.out.println("Resultados de búsqueda: " + searchResults.size() + " usuarios");
            
            // Paginación
            System.out.println("\n--- Paginación ---");
            List<User> paginatedUsers = service.getUsersPaginated(0, 2);
            System.out.println("Resultados paginados: " + paginatedUsers.size() + " usuarios");
            
        } finally {
            em.close();
            emf.close();
        }
    }
    
    private static void setupTestData(EntityManager em) {
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            
            // Crear usuarios de prueba
            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("Datos de prueba creados");
            
        } catch (Exception e) {
            if (tx.isActive()) {
                tx.rollback();
            }
            e.printStackTrace();
        }
    }
}

3. Ejemplos en PHP y Python

<?php
// Implementación insegura en PHP (vulnerable)
class UnsafeLoginPHP {
    private $pdo;
    
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    
    public function loginUnsafe($username, $password) {
        // PELIGROSO - Concatenación directa de strings
        $query = "SELECT * FROM users WHERE username = '" . $username . 
                "' AND password = '" . $password . "'";
        
        echo "Query: " . $query . "\n";
        
        $stmt = $this->pdo->query($query);
        return $stmt->rowCount() > 0;
    }
}

// Implementación segura en PHP
class SafeLoginPHP {
    private $pdo;
    
    public function __construct($pdo) {
        $this->pdo = $pdo;
    }
    
    public function loginSafe($username, $password) {
        // SEGURO - 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) {
        // Validación de entrada
        if (!$this->isValidUsername($username) || !$this->isValidPassword($password)) {
            throw new InvalidArgumentException("Entrada inválida");
        }
        
        return $this->loginSafe($username, $password);
    }
    
    private function isValidUsername($username) {
        // Validación de whitelist
        return preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username);
    }
    
    private function isValidPassword($password) {
        return strlen($password) >= 8 && strlen($password) <= 100;
    }
}

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

try {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
    
    // Crear servicios
    $unsafeService = new UnsafeLoginPHP($pdo);
    $safeService = new SafeLoginPHP($pdo);
    
    // Intentos de login normales
    echo "\n--- Intentos de login normales ---\n";
    $unsafeResult = $unsafeService->loginUnsafe('admin', 'password123');
    echo "Login inseguro: " . ($unsafeResult ? 'exitoso' : 'fallido') . "\n";
    
    $safeResult = $safeService->loginSafe('admin', 'password123');
    echo "Login seguro: " . ($safeResult ? 'exitoso' : 'fallido') . "\n";
    
    // Ataque de inyección
    echo "\n--- Ataque SQL Injection ---\n";
    $injectionUsername = "admin' OR '1'='1";
    $injectionPassword = "anything";
    
    echo "Usuario de inyección: $injectionUsername\n";
    echo "Contraseña de inyección: $injectionPassword\n";
    
    $unsafeInjectionResult = $unsafeService->loginUnsafe($injectionUsername, $injectionPassword);
    echo "Login inseguro (inyección): " . 
         ($unsafeInjectionResult ? 'EXITOSO (¡Peligro!)' : 'fallido') . "\n";
    
    $safeInjectionResult = $safeService->loginSafe($injectionUsername, $injectionPassword);
    echo "Login seguro (inyección): " . 
         ($safeInjectionResult ? 'exitoso' : 'fallido') . "\n";
    
} catch (PDOException $e) {
    echo "Error de base de datos: " . $e->getMessage() . "\n";
}
?>
# Implementación de SQLAlchemy en Python
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):
        # Validación de entrada
        if not self.is_valid_input(username) or not self.is_valid_input(password):
            raise ValueError("Entrada inválida")
        
        # Consulta SQLAlchemy - automáticamente segura
        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("Término de búsqueda inválido")
        
        # Búsqueda segura con 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):
        # Validación de 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):
        # Los términos de búsqueda permiten más caracteres
        pattern = r'^[a-zA-Z0-9_@.-\s]{1,50}$'
        return term is not None and re.match(pattern, term) is not None

# Demo en Python
def main():
    print("=== Python SQLAlchemy Security Demo ===")
    
    # Crear engine de base de datos
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    
    Session = sessionmaker(bind=engine)
    session = Session()
    
    try:
        # Crear datos de prueba
        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()
        
        # Crear servicio
        service = SecureUserService(session)
        
        # Autenticación normal
        print("\n--- Autenticación normal ---")
        user = service.authenticate('admin', 'password123')
        print(f"Autenticación: {'exitosa' if user else 'fallida'}")
        
        # Intento de inyección
        print("\n--- Intento de inyección ---")
        try:
            injected_user = service.authenticate("admin' OR '1'='1", "anything")
            print(f"Autenticación por inyección: {'exitosa (¡Peligro!)' if injected_user else 'fallida'}")
        except Exception as e:
            print(f"Autenticación por inyección: fallida (protegida)")
        
        # Búsqueda segura
        print("\n--- Búsqueda segura ---")
        search_results = service.search_users('admin')
        print(f"Resultados de búsqueda: {len(search_results)} usuarios")
        
    finally:
        session.close()

if __name__ == "__main__":
    main()

Técnicas de SQL Injection

Inyección basada en UNION

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

-- Inyección
SELECT * FROM users WHERE username = 'admin' UNION SELECT table_name, null, null, null FROM information_schema.tables--' AND password = 'anything'

Inyección basada en booleanos

-- Consulta original
SELECT * FROM products WHERE category = 'electronics'

-- Inyección
SELECT * FROM products WHERE category = 'electronics' AND 1=1--'  -- Siempre verdadero
SELECT * FROM products WHERE category = 'electronics' AND 1=0--'  -- Siempre falso

Inyección basada en tiempo

-- Query original
SELECT * FROM users WHERE id = 1

-- Inyección con retraso de tiempo
SELECT * FROM users WHERE id = 1 AND (SELECT SLEEP(5))--'  -- Retrasa la respuesta si es verdadero

Resumen de medidas de protección

MedidaDescripciónEfectividad
Prepared StatementsConsultas parametrizadasMuy alta
ORM FrameworksCapa de abstracciónAlta
Validación de entradaVerificación basada en whitelistMedia
Least PrivilegePermisos mínimos en BDAlta
Manejo de erroresSin exponer errores de BDMedia
Rate LimitingLimitación de intentosMedia

Patrones de validación de entrada

Validación por whitelist

// Solo caracteres permitidos
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]{3,20}$");
boolean isValid = pattern.matcher(input).matches();

Validación de longitud

// Limitación de longitud
if (input == null || input.length() < 3 || input.length() > 100) {
    throw new IllegalArgumentException("Longitud inválida");
}

Validación de tipo de contenido

// Verificar tipos de datos esperados
try {
    int number = Integer.parseInt(input);
} catch (NumberFormatException e) {
    throw new IllegalArgumentException("Se esperaba un número");
}

Mejores prácticas de seguridad en base de datos

Principio de Least Privilege

-- Usuario de aplicación web con permisos mínimos
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';
-- SIN permisos DROP, ALTER, CREATE

Procedimientos almacenados

-- Procedimiento almacenado seguro
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;

-- Invocación desde la aplicación
CALL authenticate_user(?, ?);

Monitoreo y detección

Detección de anomalías

// Reconocer patrones de consulta inusual
public class QueryMonitor {
    private Map<String, Integer> queryPatterns = new ConcurrentHashMap<>();
    
    public void monitorQuery(String query, String ip) {
        // Analizar patrón de consulta
        String pattern = extractPattern(query);
        
        // Verificar frecuencia
        int count = queryPatterns.merge(pattern, 1, Integer::sum);
        
        if (count > 100) { // Umbral
            alertSecurityTeam("Patrón de consulta sospechoso", ip, pattern);
        }
    }
    
    private String extractPattern(String query) {
        // Normalizar consulta para reconocimiento de patrón
        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);
        // Enviar a sistema SIEM
    }
}

Ventajas y desventajas

Ventajas de las medidas de protección

  • Seguridad: Previene comprometer la base de datos
  • Cumplimiento: Satisface estándares de seguridad
  • Confianza: Protege los datos del usuario
  • Estabilidad: Previene corrupción de datos

Desventajas

  • Rendimiento: Pequeña sobrecarga por validación
  • Complejidad: Código adicional necesario
  • Mantenimiento: Actualizaciones regulares requeridas
  • Falsos positivos: Posibles alertas incorrectas

Preguntas frecuentes de examen

  1. ¿Qué es SQL Injection y cómo funciona? Inyectar comandos SQL a través de campos de entrada concatenando strings en consultas.

  2. ¿Cuál es la mejor defensa contra SQL Injection? Los Prepared Statements con parametrización son la medida más efectiva.

  3. ¿Por qué son más seguros los ORM Frameworks? Abstraen las consultas SQL y utilizan automáticamente Prepared Statements.

  4. ¿Cuál es la diferencia entre validación whitelist y blacklist? Whitelist solo permite caracteres definidos, blacklist bloquea patrones conocidos como peligrosos.

Recursos principales

  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
Volver al blog
Share:

Entradas relacionadas