Volver
Módulo 7 - Manejo Avanzado AVANZADO

Validación de Entradas

Crea scripts seguros y robustos validando todas las entradas del usuario

La validación de entradas es crucial para crear scripts robustos y seguros. Un script bien diseñado debe manejar graciosamente entradas inválidas, proporcionar mensajes de error claros y prevenir comportamientos inesperados.

Principios de Validación

Principios Fundamentales

  • Nunca confíes en la entrada: Valida todo lo que venga del usuario
  • Falla rápido: Detecta errores lo antes posible
  • Mensajes claros: Proporciona retroalimentación útil
  • Valores por defecto: Define comportamientos seguros
  • Sanitización: Limpia y normaliza las entradas

Validación Básica

Validaciones básicas - basica.sh
bash
#!/bin/bash

# Funciones básicas de validación

# Validar que no esté vacío
validar_no_vacio() {
    local valor="$1"
    local nombre="$2"
    
    if [ -z "$valor" ]; then
        echo "Error: $nombre no puede estar vacío" >&2
        return 1
    fi
    return 0
}

# Validar longitud mínima
validar_longitud_min() {
    local valor="$1"
    local minimo="$2"
    local nombre="$3"
    
    if [ ${#valor} -lt $minimo ]; then
        echo "Error: $nombre debe tener al menos $minimo caracteres" >&2
        return 1
    fi
    return 0
}

# Validar longitud máxima
validar_longitud_max() {
    local valor="$1"
    local maximo="$2"
    local nombre="$3"
    
    if [ ${#valor} -gt $maximo ]; then
        echo "Error: $nombre no puede tener más de $maximo caracteres" >&2
        return 1
    fi
    return 0
}

# Validar que sea solo letras
validar_solo_letras() {
    local valor="$1"
    local nombre="$2"
    
    if [[ ! "$valor" =~ ^[a-zA-ZáéíóúÁÉÍÓÚñÑ]+$ ]]; then
        echo "Error: $nombre debe contener solo letras" >&2
        return 1
    fi
    return 0
}

# Validar que sea solo números
validar_solo_numeros() {
    local valor="$1"
    local nombre="$2"
    
    if [[ ! "$valor" =~ ^[0-9]+$ ]]; then
        echo "Error: $nombre debe contener solo números" >&2
        return 1
    fi
    return 0
}

# Ejemplo de uso
main() {
    local nombre="$1"
    local edad="$2"
    local telefono="$3"
    
    echo "=== VALIDACIÓN DE DATOS ==="
    
    # Validar nombre
    if validar_no_vacio "$nombre" "Nombre" && \
       validar_longitud_min "$nombre" 2 "Nombre" && \
       validar_longitud_max "$nombre" 50 "Nombre" && \
       validar_solo_letras "$nombre" "Nombre"; then
        echo "✓ Nombre válido: $nombre"
    else
        echo "✗ Nombre inválido"
        return 1
    fi
    
    # Validar edad
    if validar_no_vacio "$edad" "Edad" && \
       validar_solo_numeros "$edad" "Edad"; then
        if [ "$edad" -ge 0 ] && [ "$edad" -le 120 ]; then
            echo "✓ Edad válida: $edad años"
        else
            echo "✗ Edad debe estar entre 0 y 120 años"
            return 1
        fi
    else
        echo "✗ Edad inválida"
        return 1
    fi
    
    # Validar teléfono (opcional)
    if [ -n "$telefono" ]; then
        if validar_solo_numeros "$telefono" "Teléfono" && \
           validar_longitud_min "$telefono" 10 "Teléfono" && \
           validar_longitud_max "$telefono" 15 "Teléfono"; then
            echo "✓ Teléfono válido: $telefono"
        else
            echo "✗ Teléfono inválido"
            return 1
        fi
    else
        echo "ℹ Teléfono no proporcionado (opcional)"
    fi
    
    echo "=== TODOS LOS DATOS SON VÁLIDOS ==="
    return 0
}

# Ejecutar si se llama directamente
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
    main "$@"
fi
./basica.sh "Juan Pérez" 30 "1234567890"
./basica.sh "" 25
./basica.sh "Ana" abc

Validación con Expresiones Regulares

Validación con regex - regex_validator.sh
bash
#!/bin/bash

# Validaciones avanzadas usando expresiones regulares

# Validar formato de email
validar_email() {
    local email="$1"
    local patron='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    
    if [[ ! "$email" =~ $patron ]]; then
        echo "Error: Formato de email inválido: $email" >&2
        echo "Formato esperado: [email protected]" >&2
        return 1
    fi
    return 0
}

# Validar URL
validar_url() {
    local url="$1"
    local patron='^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$'
    
    if [[ ! "$url" =~ $patron ]]; then
        echo "Error: Formato de URL inválido: $url" >&2
        echo "Formato esperado: http://ejemplo.com o https://ejemplo.com/ruta" >&2
        return 1
    fi
    return 0
}

# Validar dirección IP
validar_ip() {
    local ip="$1"
    local patron='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
    
    if [[ ! "$ip" =~ $patron ]]; then
        echo "Error: Formato de IP inválido: $ip" >&2
        return 1
    fi
    
    # Validar rangos (0-255)
    IFS='.' read -ra OCTETOS <<< "$ip"
    for octeto in "${OCTETOS[@]}"; do
        if [ "$octeto" -gt 255 ] || [ "$octeto" -lt 0 ]; then
            echo "Error: Octeto inválido en IP: $octeto (debe estar entre 0-255)" >&2
            return 1
        fi
    done
    
    return 0
}

# Validar número de teléfono (formato internacional)
validar_telefono_internacional() {
    local telefono="$1"
    local patron='^\+[1-9][0-9]{1,3}[0-9]{4,14}$'
    
    if [[ ! "$telefono" =~ $patron ]]; then
        echo "Error: Formato de teléfono inválido: $telefono" >&2
        echo "Formato esperado: +[código país][número] (ej: +1234567890)" >&2
        return 1
    fi
    return 0
}

# Validar contraseña segura
validar_password_segura() {
    local password="$1"
    local errores=0
    
    # Al menos 8 caracteres
    if [ ${#password} -lt 8 ]; then
        echo "Error: La contraseña debe tener al menos 8 caracteres" >&2
        ((errores++))
    fi
    
    # Al menos una mayúscula
    if [[ ! "$password" =~ [A-Z] ]]; then
        echo "Error: La contraseña debe tener al menos una mayúscula" >&2
        ((errores++))
    fi
    
    # Al menos una minúscula
    if [[ ! "$password" =~ [a-z] ]]; then
        echo "Error: La contraseña debe tener al menos una minúscula" >&2
        ((errores++))
    fi
    
    # Al menos un número
    if [[ ! "$password" =~ [0-9] ]]; then
        echo "Error: La contraseña debe tener al menos un número" >&2
        ((errores++))
    fi
    
    # Al menos un carácter especial
    if [[ ! "$password" =~ [^a-zA-Z0-9] ]]; then
        echo "Error: La contraseña debe tener al menos un carácter especial" >&2
        ((errores++))
    fi
    
    return $errores
}

# Validar código postal (formato español)
validar_codigo_postal_es() {
    local cp="$1"
    local patron='^[0-5][0-9]{4}$'
    
    if [[ ! "$cp" =~ $patron ]]; then
        echo "Error: Código postal inválido: $cp" >&2
        echo "Formato esperado: 5 dígitos (00000-59999)" >&2
        return 1
    fi
    return 0
}

# Validar fecha (formato YYYY-MM-DD)
validar_fecha() {
    local fecha="$1"
    local patron='^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
    
    if [[ ! "$fecha" =~ $patron ]]; then
        echo "Error: Formato de fecha inválido: $fecha" >&2
        echo "Formato esperado: YYYY-MM-DD" >&2
        return 1
    fi
    
    # Validar usando date
    if ! date -d "$fecha" &>/dev/null; then
        echo "Error: Fecha no válida: $fecha" >&2
        return 1
    fi
    
    return 0
}

# Función de demostración
demo_validaciones() {
    echo "=== DEMO DE VALIDACIONES CON REGEX ==="
    
    # Emails
    echo -e "\n--- EMAILS ---"
    validar_email "[email protected]" && echo "✓ Email válido"
    validar_email "correo_incorrecto" || echo "✗ Email inválido (esperado)"
    
    # URLs
    echo -e "\n--- URLs ---"
    validar_url "https://www.ejemplo.com" && echo "✓ URL válida"
    validar_url "no-es-url" || echo "✗ URL inválida (esperado)"
    
    # IPs
    echo -e "\n--- DIRECCIONES IP ---"
    validar_ip "192.168.1.1" && echo "✓ IP válida"
    validar_ip "999.999.999.999" || echo "✗ IP inválida (esperado)"
    
    # Teléfonos
    echo -e "\n--- TELÉFONOS ---"
    validar_telefono_internacional "+1234567890" && echo "✓ Teléfono válido"
    validar_telefono_internacional "123456" || echo "✗ Teléfono inválido (esperado)"
    
    # Contraseñas
    echo -e "\n--- CONTRASEÑAS ---"
    validar_password_segura "MiPass123!" && echo "✓ Contraseña segura"
    validar_password_segura "123" || echo "✗ Contraseña insegura (esperado)"
    
    # Fechas
    echo -e "\n--- FECHAS ---"
    validar_fecha "2024-12-25" && echo "✓ Fecha válida"
    validar_fecha "2024-13-45" || echo "✗ Fecha inválida (esperado)"
}

# Ejecutar demo si se llama sin argumentos
if [ $# -eq 0 ]; then
    demo_validaciones
fi

Validación de Archivos y Directorios

Validación de archivos - file_validator.sh
bash
#!/bin/bash

# Validaciones para archivos y directorios

# Validar que el archivo existe
validar_archivo_existe() {
    local archivo="$1"
    local nombre="${2:-archivo}"
    
    if [ ! -f "$archivo" ]; then
        echo "Error: El $nombre '$archivo' no existe" >&2
        return 1
    fi
    return 0
}

# Validar que el archivo es legible
validar_archivo_legible() {
    local archivo="$1"
    local nombre="${2:-archivo}"
    
    if [ ! -r "$archivo" ]; then
        echo "Error: No se puede leer el $nombre '$archivo'" >&2
        return 1
    fi
    return 0
}

# Validar que el archivo es escribible
validar_archivo_escribible() {
    local archivo="$1"
    local nombre="${2:-archivo}"
    
    if [ ! -w "$archivo" ]; then
        echo "Error: No se puede escribir en el $nombre '$archivo'" >&2
        return 1
    fi
    return 0
}

# Validar que el archivo es ejecutable
validar_archivo_ejecutable() {
    local archivo="$1"
    local nombre="${2:-archivo}"
    
    if [ ! -x "$archivo" ]; then
        echo "Error: El $nombre '$archivo' no es ejecutable" >&2
        return 1
    fi
    return 0
}

# Validar tamaño de archivo
validar_tamaño_archivo() {
    local archivo="$1"
    local tamaño_max="$2"  # en bytes
    local nombre="${3:-archivo}"
    
    if [ ! -f "$archivo" ]; then
        echo "Error: El $nombre '$archivo' no existe" >&2
        return 1
    fi
    
    local tamaño_actual=$(stat -f%z "$archivo" 2>/dev/null || stat -c%s "$archivo" 2>/dev/null)
    
    if [ "$tamaño_actual" -gt "$tamaño_max" ]; then
        echo "Error: El $nombre '$archivo' es demasiado grande ($(($tamaño_actual/1024))KB > $(($tamaño_max/1024))KB)" >&2
        return 1
    fi
    return 0
}

# Validar extensión de archivo
validar_extension() {
    local archivo="$1"
    local extension_esperada="$2"
    
    local extension_real="${archivo##*.}"
    
    if [ "$extension_real" != "$extension_esperada" ]; then
        echo "Error: El archivo debe tener extensión .$extension_esperada (encontrada: .$extension_real)" >&2
        return 1
    fi
    return 0
}

# Validar tipo MIME
validar_tipo_mime() {
    local archivo="$1"
    local tipo_esperado="$2"
    
    if ! command -v file &> /dev/null; then
        echo "Error: El comando 'file' no está disponible" >&2
        return 1
    fi
    
    local tipo_real=$(file -b --mime-type "$archivo" 2>/dev/null)
    
    if [ "$tipo_real" != "$tipo_esperado" ]; then
        echo "Error: Tipo de archivo incorrecto (esperado: $tipo_esperado, encontrado: $tipo_real)" >&2
        return 1
    fi
    return 0
}

# Validar directorio
validar_directorio() {
    local directorio="$1"
    local nombre="${2:-directorio}"
    
    if [ ! -d "$directorio" ]; then
        echo "Error: El $nombre '$directorio' no existe o no es un directorio" >&2
        return 1
    fi
    return 0
}

# Validar directorio escribible
validar_directorio_escribible() {
    local directorio="$1"
    local nombre="${2:-directorio}"
    
    if ! validar_directorio "$directorio" "$nombre"; then
        return 1
    fi
    
    if [ ! -w "$directorio" ]; then
        echo "Error: No se puede escribir en el $nombre '$directorio'" >&2
        return 1
    fi
    return 0
}

# Validar espacio libre en directorio
validar_espacio_libre() {
    local directorio="$1"
    local espacio_min="$2"  # en KB
    
    local espacio_libre=$(df "$directorio" | awk 'NR==2 {print $4}')
    
    if [ "$espacio_libre" -lt "$espacio_min" ]; then
        echo "Error: No hay suficiente espacio libre en '$directorio'" >&2
        echo "Requerido: ${espacio_min}KB, disponible: ${espacio_libre}KB" >&2
        return 1
    fi
    return 0
}

# Función integral para validar archivo de entrada
validar_archivo_entrada() {
    local archivo="$1"
    local max_size="${2:-1048576}"  # 1MB por defecto
    
    echo "Validando archivo de entrada: $archivo"
    
    # Validaciones en cadena
    if validar_archivo_existe "$archivo" && \
       validar_archivo_legible "$archivo" && \
       validar_tamaño_archivo "$archivo" "$max_size"; then
        echo "✓ Archivo de entrada válido"
        return 0
    else
        echo "✗ Archivo de entrada inválido"
        return 1
    fi
}

# Función integral para validar directorio de salida
validar_directorio_salida() {
    local directorio="$1"
    local espacio_min="${2:-10240}"  # 10MB por defecto
    
    echo "Validando directorio de salida: $directorio"
    
    if validar_directorio "$directorio" "directorio de salida" && \
       validar_directorio_escribible "$directorio" "directorio de salida" && \
       validar_espacio_libre "$directorio" "$espacio_min"; then
        echo "✓ Directorio de salida válido"
        return 0
    else
        echo "✗ Directorio de salida inválido"
        return 1
    fi
}

# Ejemplo de uso
main() {
    if [ $# -lt 2 ]; then
        echo "Uso: $0 archivo_entrada directorio_salida"
        exit 1
    fi
    
    local archivo_entrada="$1"
    local directorio_salida="$2"
    
    echo "=== VALIDACIÓN DE ARCHIVOS Y DIRECTORIOS ==="
    
    # Validar archivo de entrada
    if ! validar_archivo_entrada "$archivo_entrada"; then
        exit 1
    fi
    
    echo ""
    
    # Validar directorio de salida
    if ! validar_directorio_salida "$directorio_salida"; then
        exit 1
    fi
    
    echo ""
    echo "✓ Todas las validaciones pasaron correctamente"
    echo "Listo para procesar '$archivo_entrada' hacia '$directorio_salida'"
}

# Ejecutar si se llama directamente
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
    main "$@"
fi

Sistema de Validación Robusto

Sistema completo - validator_framework.sh
bash
#!/bin/bash

# Framework de validación robusto para Bash

# Variables globales
declare -g VALIDATION_ERRORS=()
declare -g VALIDATION_WARNINGS=()
declare -g VALIDATION_STRICT=true

# Limpiar errores y advertencias
validation_reset() {
    VALIDATION_ERRORS=()
    VALIDATION_WARNINGS=()
}

# Agregar error de validación
validation_add_error() {
    local mensaje="$1"
    VALIDATION_ERRORS+=("ERROR: $mensaje")
}

# Agregar advertencia de validación
validation_add_warning() {
    local mensaje="$1"
    VALIDATION_WARNINGS+=("WARN: $mensaje")
}

# Verificar si hay errores
validation_has_errors() {
    [ ${#VALIDATION_ERRORS[@]} -gt 0 ]
}

# Verificar si hay advertencias
validation_has_warnings() {
    [ ${#VALIDATION_WARNINGS[@]} -gt 0 ]
}

# Mostrar todos los errores y advertencias
validation_show_results() {
    local show_warnings="${1:-true}"
    
    if validation_has_errors; then
        echo "❌ ERRORES DE VALIDACIÓN:" >&2
        printf '%s\n' "${VALIDATION_ERRORS[@]}" >&2
        echo "" >&2
    fi
    
    if [ "$show_warnings" = true ] && validation_has_warnings; then
        echo "⚠️  ADVERTENCIAS DE VALIDACIÓN:" >&2
        printf '%s\n' "${VALIDATION_WARNINGS[@]}" >&2
        echo "" >&2
    fi
}

# Validador genérico
validate() {
    local valor="$1"
    local tipo="$2"
    local nombre="$3"
    local opciones="$4"
    
    case "$tipo" in
        "required")
            if [ -z "$valor" ]; then
                validation_add_error "$nombre es requerido"
                return 1
            fi
            ;;
        "email")
            if [[ ! "$valor" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
                validation_add_error "$nombre debe ser un email válido"
                return 1
            fi
            ;;
        "url")
            if [[ ! "$valor" =~ ^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$ ]]; then
                validation_add_error "$nombre debe ser una URL válida"
                return 1
            fi
            ;;
        "ip")
            if ! validate_ip_address "$valor"; then
                validation_add_error "$nombre debe ser una dirección IP válida"
                return 1
            fi
            ;;
        "number")
            if [[ ! "$valor" =~ ^-?[0-9]+$ ]]; then
                validation_add_error "$nombre debe ser un número entero"
                return 1
            fi
            ;;
        "positive")
            validate "$valor" "number" "$nombre"
            if [ $? -eq 0 ] && [ "$valor" -le 0 ]; then
                validation_add_error "$nombre debe ser un número positivo"
                return 1
            fi
            ;;
        "range")
            validate "$valor" "number" "$nombre"
            if [ $? -eq 0 ]; then
                IFS='-' read -ra RANGE <<< "$opciones"
                local min="${RANGE[0]}"
                local max="${RANGE[1]}"
                if [ "$valor" -lt "$min" ] || [ "$valor" -gt "$max" ]; then
                    validation_add_error "$nombre debe estar entre $min y $max"
                    return 1
                fi
            fi
            ;;
        "length")
            IFS='-' read -ra RANGE <<< "$opciones"
            local min="${RANGE[0]:-0}"
            local max="${RANGE[1]:-999999}"
            if [ ${#valor} -lt "$min" ] || [ ${#valor} -gt "$max" ]; then
                validation_add_error "$nombre debe tener entre $min y $max caracteres"
                return 1
            fi
            ;;
        "file")
            if [ ! -f "$valor" ]; then
                validation_add_error "$nombre debe ser un archivo existente"
                return 1
            fi
            ;;
        "dir")
            if [ ! -d "$valor" ]; then
                validation_add_error "$nombre debe ser un directorio existente"
                return 1
            fi
            ;;
        "readable")
            if [ ! -r "$valor" ]; then
                validation_add_error "$nombre debe ser legible"
                return 1
            fi
            ;;
        "writable")
            if [ ! -w "$valor" ]; then
                validation_add_error "$nombre debe ser escribible"
                return 1
            fi
            ;;
        "executable")
            if [ ! -x "$valor" ]; then
                validation_add_error "$nombre debe ser ejecutable"
                return 1
            fi
            ;;
        *)
            validation_add_error "Tipo de validación desconocido: $tipo"
            return 1
            ;;
    esac
    
    return 0
}

# Función auxiliar para validar IP
validate_ip_address() {
    local ip="$1"
    
    if [[ ! "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        return 1
    fi
    
    IFS='.' read -ra OCTETS <<< "$ip"
    for octet in "${OCTETS[@]}"; do
        if [ "$octet" -gt 255 ] || [ "$octet" -lt 0 ]; then
            return 1
        fi
    done
    
    return 0
}

# Validador de esquemas complejos
validate_schema() {
    local -n schema_ref=$1
    local -n data_ref=$2
    
    validation_reset
    
    for field in "${!schema_ref[@]}"; do
        local rules="${schema_ref[$field]}"
        local value="${data_ref[$field]}"
        
        # Procesar múltiples reglas separadas por |
        IFS='|' read -ra RULES <<< "$rules"
        for rule in "${RULES[@]}"; do
            IFS=':' read -ra RULE_PARTS <<< "$rule"
            local rule_type="${RULE_PARTS[0]}"
            local rule_option="${RULE_PARTS[1]:-}"
            
            validate "$value" "$rule_type" "$field" "$rule_option"
        done
    done
}

# Ejemplo de uso del framework
demo_validation_framework() {
    echo "=== DEMO DEL FRAMEWORK DE VALIDACIÓN ==="
    
    # Definir esquema de validación
    declare -A user_schema=(
        ["name"]="required|length:2-50"
        ["email"]="required|email"
        ["age"]="required|positive|range:18-120"
        ["website"]="url"
        ["config_file"]="file|readable"
        ["output_dir"]="dir|writable"
    )
    
    # Datos de ejemplo (algunos inválidos intencionalmente)
    declare -A user_data=(
        ["name"]="Juan Pérez"
        ["email"]="[email protected]"
        ["age"]="25"
        ["website"]="https://ejemplo.com"
        ["config_file"]="/etc/passwd"  # Existe pero puede no ser legible
        ["output_dir"]="/tmp"
    )
    
    echo "Validando datos del usuario..."
    validate_schema user_schema user_data
    
    if validation_has_errors; then
        echo "❌ Validación fallida:"
        validation_show_results
        return 1
    else
        echo "✅ Validación exitosa:"
        if validation_has_warnings; then
            validation_show_results
        else
            echo "No se encontraron errores ni advertencias"
        fi
        return 0
    fi
}

# Función principal para demostración
main() {
    if [ $# -eq 0 ]; then
        demo_validation_framework
    else
        echo "Framework de validación cargado"
        echo "Use las funciones validate() y validate_schema() en sus scripts"
    fi
}

# Ejecutar si se llama directamente
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
    main "$@"
fi

Mejores Prácticas de Validación

  • Valida temprano: Haz todas las validaciones antes de comenzar el procesamiento principal
  • Mensajes específicos: Proporciona mensajes de error claros y accionables
  • Códigos de salida: Usa códigos de salida consistentes (0=éxito, 1=error de validación, etc.)
  • Funciones reutilizables: Crea bibliotecas de funciones de validación
  • Documentación: Documenta qué formatos acepta tu script
  • Valores por defecto: Define comportamientos seguros para valores opcionales

Consideraciones de Seguridad

  • Sanitización: Limpia entradas antes de usarlas en comandos del sistema
  • Inyección de comandos: Nunca uses entrada del usuario directamente en eval o como comandos
  • Rutas relativas: Valida que las rutas no contengan ".." para evitar directory traversal
  • Límites: Establece límites en tamaños de archivos y longitud de cadenas
  • Permisos: Verifica permisos antes de intentar operaciones en archivos

Ejercicios Prácticos

Ejercicio 1: Validador de Configuración

Crea un script que valide un archivo de configuración:

  • Verifica que todas las claves requeridas estén presentes
  • Valida formatos de URLs, emails y números de puerto
  • Comprueba que los archivos y directorios especificados existen
  • Proporciona un reporte detallado de errores y advertencias

Ejercicio 2: Sistema de Registro de Usuarios

Desarrolla un script de registro que:

  • Valide nombres de usuario únicos
  • Verifique la fortaleza de contraseñas
  • Confirme formatos de email y teléfono
  • Implemente un sistema de rollback en caso de errores