31

Seguridad en Scripts Bash

Vistas: 0
Tutorial: Scripts en Bash
Seguridad en Scripts Bash

En le capítulo anterior vimos como conseguir que tus scripts notifiquen, generan reportes y hablan con APIs. Pero con más poder llega más responsabilidad. Cuando escribes un script Bash, es fácil pensar que solo tú lo ejecutarás, en tu máquina, con tus datos. Pero los scripts crecen, se comparten, se automatizan con cron, se ejecutan como root, reciben entrada de usuarios o APIs.

Un fallo de seguridad en un script puede ser catastrófico:

  • Una variable sin comillas puede ejecutar un comando arbitrario
  • Un eval sobre entrada de usuario puede borrar todo el sistema
  • Un archivo temporal con nombre predecible puede ser secuestrado
  • Una variable de entorno filtrada puede exponer contraseñas

¿Por qué es importante la seguridad en scripts?

Cuando escribes un script Bash, es fácil pensar que solo tú lo ejecutarás, en tu máquina, con tus datos. Pero los scripts crecen, se comparten, se automatizan con cron, se ejecutan como root, reciben entrada de usuarios o APIs, y acaban haciendo cosas como borrar archivos, conectarse a bases de datos o manejar claves SSH.

Sanitización de entrada

Toda entrada externa — argumentos, stdin, variables de entorno, archivos, APIs — es potencialmente maliciosa. Nunca confíes en ella.

printf %q — Escapar para shells

printf %q escapa una cadena de forma que el shell la interprete como un único token seguro:

# Escapar un argumento para usarlo en comandos
input="archivo con espacios; rm -rf /"
seguro=$(printf '%q' "$input")
echo "$seguro"
# Output: archivo\ con\ espacios\;\ rm\ -rf\ /

Usos típicos:

# Construir comandos seguros con ssh
comando=$(printf '%q' "$input")
ssh servidor "ls -la $comando"

# Pasar argumentos a otro script
safe=$(printf '%q' "$1")
./otro_script.sh "$safe"

[[ =~ ]] — Validación con expresiones regulares

La validación más segura es rechazar lo que no coincide con un patrón esperado, no tratar de filtrar lo malo:

validar_email() {
    local email="$1"
    # Patrón estricto de email
    if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
        return 0
    fi
    return 1
}

validar_ip() {
    local ip="$1"
    if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        # Validación adicional de rangos
        local IFS='.'
        read -r o1 o2 o3 o4 <<< "$ip"
        (( o1 <= 255 && o2 <= 255 && o3 <= 255 && o4 <= 255 ))
        return $?
    fi
    return 1
}

validar_nombre_archivo() {
    local nombre="$1"
    # Solo alfanumérico, guiones, puntos y guiones bajos
    # Sin barras, sin espacios, sin caracteres de control
    if [[ "$nombre" =~ ^[a-zA-Z0-9._-]+$ ]]; then
        return 0
    fi
    return 1
}

Parámetros por expansión — Manipulación segura

Bash ofrece expansiones de parámetros que sanitizan cadenas sin ejecutar nada:

# Eliminar caracteres no deseados (solo dígitos)
num="${input_numero//[^0-9]/}"

# Eliminar caracteres de control y no imprimibles
limpio="${input//[^[:print:]]/}"

# Convertir a minúsculas
minus="${input,,}"

# Truncar longitud máxima
corto="${input:0:50}"

# Valor por defecto si está vacío
nombre="${1:-invitado}"

Combinación de técnicas — validación en capas:

sanitizar_nombre_usuario() {
    local input="$1"
    local max_len="${2:-32}"

    # Eliminar caracteres no ASCII y peligrosos
    local limpio="${input//[^a-zA-Z0-9._-]/}"

    # Truncar longitud
    limpio="${limpio:0:$max_len}"

    # Validar que no esté vacío tras sanitizar
    if [[ -z "$limpio" ]]; then
        echo "ERROR: Nombre de usuario inválido" >&2
        return 1
    fi

    echo "$limpio"
}

Inyección de comandos

El peligro de eval

eval ejecuta su argumento como si fuera código Bash escrito directamente. Esto es extremadamente peligroso con entrada externa:

# ❌ NUNCA hagas esto
nombre="$1"
eval "echo Hola $nombre"
# Input: "; rm -rf /; "
# Ejecuta: echo Hola ; rm -rf / ;
# ¡adiós sistema!

# ❌ Otro ejemplo clásico — eval sobre vars de entorno
eval "comando=\$$1"
# Si $1 es "HOME; rm -rf /", desastre

Regla de oro: si puedes evitar eval, evítalo. Si crees que no puedes, piensa más. Hay una alternativa.

Alternativas seguras a eval

# ❌ Innecesario
eval "echo $VAR"

# ✅ Directo
echo "$VAR"


# ❌ Variables indirectas
eval "valor=\$$nombre_var"

# ✅ Expansión indirecta
valor="${!nombre_var}"


# ❌ Construcción de comandos
eval "$comando $args"

# ✅ Arrays
comando=( "$COMANDO_BASE" "$arg1" "$arg2" )
"${comando[@]}"


# ❌ Matemáticas con eval
eval "resultado=\$(( $expr ))"

# ✅ Aritmética segura
resultado=$(( expr ))


# ❌ Parseo con eval + getopt
args=$(getopt -o "ab:c" -- "$@")
eval set -- "$args"

# ✅ Parseo directo
while getopts ":ab:c" opt; do
    case "$opt" in
        a) echo "Opción -a" ;;        # Procesar flag -a
        b) echo "Opción -b: $OPTARG" ;;  # Procesar -b con argumento
        c) echo "Opción -c" ;;        # Procesar flag -c
        \?) echo "Opción inválida" >&2 ;;
    esac
done

Si ABSOLUTAMENTE necesitas eval (casos muy raros como generación dinámica de código), sanitiza incondicionalmente:

# El único caso aceptable: datos completamente controlados
eval "$(printf '%q' "$comando_seguro")"

Path traversal y manipulación de rutas

basename y realpath

El ataque de path traversal permite acceder a archivos fuera del directorio esperado usando ../:

# ❌ El usuario pasa "../../etc/passwd"
archivo="/var/log/$1"
cat "$archivo"  # ¡cat /var/log/../../etc/passwd = /etc/passwd!

# ✅ Usar basename para eliminar componentes de ruta
archivo="/var/log/$(basename "$1")"
cat "$archivo"  # Solo "passwd" → /var/log/passwd (no existe)

# ✅ Usar realpath para resolver symlinks y ..
ruta_real=$(realpath "$1" 2>/dev/null) || { echo "Ruta inválida"; exit 1; }

# Verificar que la ruta está dentro de un directorio permitido
if [[ "$ruta_real" != /var/log/allowed/* ]]; then
    echo "Acceso denegado: $ruta_real" >&2
    exit 1
fi

Validación de rutas segura

es_ruta_segura() {
    local ruta="$1"
    local permitido="$2"

    # Obtener ruta absoluta canónica
    local real
    real=$(realpath -m "$ruta" 2>/dev/null) || return 1

    # Verificar que empieza con el directorio permitido
    if [[ "$real" != "$permitido"* ]]; then
        return 1
    fi

    # Verificar que no hay componentes peligrosos residuales
    if [[ "$ruta" == *".."* || "$ruta" == *"~"* || "$ruta" == *"//"* ]]; then
        return 1
    fi

    return 0
}

# Uso
if es_ruta_segura "$1" "/var/log/mi_app"; then
    cat "$1"
else
    echo "Acceso denegado" >&2
    exit 1
fi

Permisos de archivos

umask — Permisos por defecto seguros

umask determina los permisos que no se asignan a los archivos nuevos. Un valor seguro elimina permisos de grupo y otros:

# umask actual
umask  # (por defecto en muchos sistemas)

# Más restrictivo — solo el propietario escribe
umask 0077
touch secreto.txt
ls -l secreto.txt  # -rw-------

# Balanceado — propietario escribe, grupo/otros leen
umask 0022
touch publico.txt
ls -l publico.txt  # -rw-r--r--

# Extremo — solo propietario lee y escribe
umask 0177
touch ultra.txt
ls -l ultra.txt  # -rw-------

En scripts que manejan datos sensibles, establece umask al inicio:

#!/bin/bash
umask 0077  # Todos los archivos creados: solo el usuario

chmod y chown — Permisos específicos

# Script ejecutable solo por el propietario
chmod 700 script.sh

# Directorio de trabajo accesible solo por el grupo
chmod 750 /opt/mi_app/
chown root:appusers /opt/mi_app/

# Archivo de configuración con secretos
chmod 600 /etc/mi_app/secrets.env
chown root:root /etc/mi_app/secrets.env

# Dar permisos de ejecución sin abrir escritura a otros
chmod 755 bin/herramienta.sh  # rwxr-xr-x — ejecutable para todos, solo root escribe

Nunca chmod 777. Es el equivalente a pegar un cartel de «cualquiera puede modificar esto, incluyendo malware».

Verificar permisos antes de operar

verificar_permisos() {
    local archivo="$1"
    local min_perm="${2:-600}"

    if [[ ! -f "$archivo" ]]; then
        echo "ERROR: Archivo no existe: $archivo" >&2
        return 1
    fi

    local actual
    actual=$(stat -c "%a" "$archivo")

    # Los permisos actuales deben ser igual o más restrictivos que min_perm
    if (( actual > min_perm )); then
        echo "ERROR: Permisos demasiado abiertos en $archivo: $actual (máx: $min_perm)" >&2
        return 1
    fi

    echo "✓ Permisos OK en $archivo: $actual"
    return 0
}

Archivos temporales seguros

mktemp con trap cleanup

Crear archivos temporales con nombres predecibles (/tmp/mi_script.tmp) es peligroso: un atacante puede crear un enlace simbólico con ese nombre y redirigir la escritura a otro archivo.

#!/bin/bash
set -euo pipefail

# Crear archivo temporal seguro
TEMP_FILE=$(mktemp /tmp/mi_script.XXXXXX)
TEMP_DIR=$(mktemp -d /tmp/mi_script.XXXXXX)

# Asegurar limpieza siempre — incluso con Ctrl+C
cleanup() {
    local exit_code=$?
    echo "🧹 Limpiando archivos temporales..." >&2
    [[ -f "$TEMP_FILE" ]] && rm -f "$TEMP_FILE"
    [[ -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
    exit "$exit_code"
}

trap cleanup EXIT INT TERM

# Trabajar con los archivos temporales
echo "datos sensibles" > "$TEMP_FILE"
procesar_datos < "$TEMP_FILE"

Evitar condiciones de carrera

Una condición de carrera (TOCTOU — Time Of Check, Time Of Use) ocurre cuando verificas un archivo y luego lo usas, pero algo cambió entre medias:

# ❌ TOCTOU: vulnerable
if [[ ! -f /tmp/lock.txt ]]; then
    # ❌ Aquí otro proceso puede crear /tmp/lock.txt
    echo "$$" > /tmp/lock.txt
fi

# ✅ mktemp es atómico — garantiza que el archivo es nuevo
LOCK_FILE=""
LOCK_FILE=$(mktemp /tmp/mi_app.lock.XXXXXX 2>/dev/null) || {
    echo "ERROR: No se pudo crear lock" >&2
    exit 1
}

# Usar el archivo como lock (creación atómica)
if ! ln -s "$LOCK_FILE" /tmp/mi_app.lock 2>/dev/null; then
    echo "ERROR: Otro proceso ya tiene el lock" >&2
    rm -f "$LOCK_FILE"
    exit 1
fi

Modo estricto: set -euo pipefail

Es la primera línea de defensa de cualquier script profesional.

set -e (errexit) — Salir ante error

Sin set -e, Bash continúa ejecutando aunque un comando falle:

# Sin set -e
rm -rf /directorio_importante
echo "¡Se borró!"  # Se ejecuta aunque rm haya fallado

# Con set -e
set -e
rm -rf /directorio_importante
echo "¡Se borró!"  # No se ejecuta si rm falla

Excepciones controladas: a veces un comando puede fallar sin ser error. Usa || true:

# grep devuelve 1 si no encuentra — puede ser aceptable
set -e
if grep -q "patron" archivo.txt; then
    echo "Encontrado"
fi
# Si grep no encuentra, set -e mata el script ❌

# ✅ Solución: encadenar con || true
if grep -q "patron" archivo.txt || true; then
    ...
fi

# O mejor: ignorar el error explícitamente
grep -q "patron" archivo.txt || [[ $? -eq 1 ]]

set -u (nounset) — Variables indefinidas son error

set -u
echo "$NOMBRE"  # script.sh: line X: NOMBRE: unbound variable → ¡MUERE!

# ✅ Siempre usa valores por defecto
echo "${NOMBRE:-invitado}"

set -o pipefail — Errores en tuberías

Sin pipefail, solo el último comando de la tubería determina el código de salida:

# Sin pipefail
grep "secreto" /etc/shadow | cut -d: -f1 | wc -l
# grep falla (permiso denegado), pero wc -l devuelve 0 → ¡éxito!

# Con pipefail
set -o pipefail
grep "secreto" /etc/shadow | cut -d: -f1 | wc -l
# Falla con el código de grep (1 o 2)

El trío completo

#!/bin/bash
set -euo pipefail

Esta línea debe estar en todos tus scripts profesionales. Punto.

Comillas y expansión de variables

La regla más simple y más violada: siempre cita las variables.

archivo="mi archivo.txt"

# ❌ Sin comillas — word splitting
rm $archivo     # rm "mi" "archivo.txt" → ¡borra dos archivos!

# ✅ Con comillas
rm "$archivo"   # rm "mi archivo.txt" → correcto

# ❌ Sin comillas en arrays
args=( -name "*.txt" -type f )
find . ${args[@]}  # Word splitting rompe el array

# ✅ Con comillas dobles
find . "${args[@]}"  # Cada elemento es un argumento

Expansiones de variables que SIEMPRE deben ir entre comillas:

ExpansiónSin comillasCon comillas
$varWord splitting + globbingUn solo argumento
"$@"Se rompeCada arg se preserva
"${arr[@]}"Se rompeArray completo
$(comando)Word splitting + globbingSalida intacta
$((expr))Generalmente seguroMás seguro
$var -> archivoArchivo con espacios → errorFunciona

Shellshock y otras vulnerabilidades Bash

Shellshock (CVE-2014-6271)

Shellshock es una vulnerabilidad en Bash que permite ejecutar comandos arbitrarios a través de variables de entorno con definiciones de función malformadas:

# Exploit Shellshock
env x='() { :;}; echo VULNERABLE' bash -c "echo prueba"
# Si es vulnerable, imprime "VULNERABLE"

¿Cómo afecta a tus scripts? Si tu script se ejecuta en un contexto donde variables de entorno externas pueden ser inyectadas (CGI, DHCP, SSH), un atacante puede ejecutar código.

Protección:

# Actualizar Bash (parcheado desde 2014)
# Validar variables de entorno externas
if [[ "$ENV_VAR" == *'() {'* ]]; then
    echo "Posible ataque Shellshock detectado" >&2
    exit 1
fi

# Reinicializar el entorno en scripts críticos
#!/bin/bash
exec env -i HOME="$HOME" PATH="$PATH" bash script_seguro.sh

Otras vulnerabilidades conocidas

CVEAñoDescripciónMitigación
CVE-2014-62712014Shellshock — eval en definición de funcionesActualizar Bash
CVE-2014-71692014Shellshock II — bypass del parcheActualizar Bash
CVE-2019-99242019Command injection en [[ con patrones especialesActualizar Bash
CVE-2022-37152022Heap overflow en printfActualizar Bash

La mejor protección: mantener Bash actualizado.

SUID/GID en scripts — Por qué es peligroso

El bit SUID (chmod u+s) hace que un ejecutable se ejecute con los permisos de su propietario (normalmente root). En binarios compilados, esto es útil. En scripts, es una puerta abierta a ataques.

# ❌ NUNCA hagas esto en un script
chmod u+s mi_script.sh  # ¡PELIGROSÍSIMO!

# ¿Por qué?

Razones por las que SUID en scripts no funciona correctamente:

  1. El shebang es inseguro: el kernel ejecuta el intérprete (/bin/bash), no el script. Si un atacante modifica PATH o BASH_ENV, puede ejecutar código como root.
  2. Las variables de entorno no se limpian: LD_PRELOAD, BASH_ENV, BASH_FUNC_* pueden inyectar código.
  3. Race conditions: entre que se verifica el script y se ejecuta, se puede reemplazar.
# Un atacante podría hacer:
export BASH_ENV=/tmp/malicioso.sh
./script_suid.sh  # Ejecuta código malicioso como root

Alternativa segura: sudo con reglas específicas.

Variables de entorno y secretos

read -s para contraseñas

read -s -p "🔑 Contraseña: " password
echo  # Salto de línea necesario tras -s
echo "Contraseña recibida (${#password} caracteres)"

# ⚠️ Para scripts críticos, limpia la variable al terminar
procesar_datos "$password"
password=""  # Limpiar de memoria (no es perfecto pero ayuda)

Archivos .env (no en git)

# .env — NUNCA hacer commit
DB_HOST=localhost
DB_USER=admin
DB_PASS=s3cr3t0!
API_KEY=sk-1234567890abcdef

# .gitignore
echo ".env" >> .gitignore

# Cargar .env de forma segura
if [[ -f .env ]]; then
    set -a  # export automático
    source .env
    set +a
fi

# ⚠️ Mejor aún: cargar solo las variables necesarias
while IFS='=' read -r key value; do
    # Ignorar comentarios y líneas vacías
    [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
    # Validar que el nombre de variable es seguro
    if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
        export "$key=$value"
    fi
done < .env

Variables con ámbito restringido

# ❌ Variable global expuesta
API_KEY="sk-123456"
funcion() {
    curl -H "Authorization: Bearer $API_KEY" ...
}
# API_KEY visible en /proc/$$/environ, ps, etc.

# ✅ Variable local en función
funcion() {
    local api_key="sk-123456"
    curl -H "Authorization: Bearer $api_key" ...
}
# api_key solo existe durante la ejecución de la función

# ✅ Exportar solo para un comando específico
DB_PASS="secreto" mi_comando --db-pass "$DB_PASS"

# ⚠️ Limpiar variables de entorno al terminar
export MI_SECRETO="valor"
...
unset MI_SECRETO  # Eliminar del entorno

Patrones seguros con sudo

Siempre aplica el principio de mínimo privilegio: da solo los permisos necesarios, solo a quien los necesita, solo por el tiempo necesario.

# ❌ Dar acceso total
# En sudoers: usuario ALL=(ALL) ALL  # ¡demasiado!

# ✅ Comandos específicos permitidos
# En sudoers: usuario ALL=(root) /usr/sbin/systemctl restart nginx, /usr/bin/grep

# En el script:
ejecutar_como_root() {
    local cmd="$1"
    # Validar que el comando está en la lista de permitidos
    case "$cmd" in
        "reiniciar_nginx")
            sudo systemctl restart nginx
            ;;
        "recargar_apache")
            sudo systemctl reload apache2
            ;;
        *)
            echo "ERROR: Comando no permitido" >&2
            return 1
            ;;
    esac
}

Patrón sudo con timeout — evitar sesiones sudo abiertas:

# Forzar sudo a pedir contraseña siempre
sudo -k  # Invalida la caché de timestamp

# Ejecutar con timeout
timeout 30 sudo comando_peligroso

# Script con sudo segmented:
sudo_no_interactivo() {
    local cmd="$1"
    if sudo -n true 2>/dev/null; then
        # sudo no necesita contraseña (ya autenticado)
        sudo "$@"
    else
        echo "Se requieren privilegios elevados para: $1" >&2
        sudo "$@"
    fi
}

Cifrado con GPG y OpenSSL

GPG — Cifrado simétrico

# Cifrar archivo con contraseña
gpg --symmetric --cipher-algo AES256 \
    --batch --passphrase "$PASS" \
    -o datos_secretos.gpg \
    datos_secretos.txt

# Descifrar
gpg --decrypt --batch --passphrase "$PASS" \
    -o datos_recuperados.txt \
    datos_secretos.gpg

# Cifrado asimétrico (clave pública)
gpg --encrypt --recipient "admin@ejemplo.com" \
    -o secreto.gpg \
    secreto.txt

OpenSSL — Alternativa ligera

# Cifrado AES-256-CBC con derivación PBKDF2
openssl enc -aes-256-cbc -salt -pbkdf2 \
    -pass pass:"$PASS" \
    -in datos.txt \
    -out datos.enc

# Descifrar
openssl enc -d -aes-256-cbc -salt -pbkdf2 \
    -pass pass:"$PASS" \
    -in datos.enc \
    -out datos_recuperados.txt

# Cifrado con clave generada desde archivo
openssl rand -base64 32 > clave_aes.key
openssl enc -aes-256-cbc -salt -pbkdf2 \
    -pass file:clave_aes.key \
    -in datos.txt \
    -out datos.enc

Nunca pases la contraseña por argumento si puedes evitarlo — otros usuarios la ven en ps:

# ❌ MAL — visible en ps
./script.sh --password "secreto"

# ✅ MEJOR — variable de entorno
export PASS="secreto"
./script.sh

# ✅ MEJOR aún — archivo con permisos 600
./script.sh --password-file /etc/secrets/pass.txt

# ⚠️ O usa openssl con file descriptor
openssl enc -aes-256-cbc -salt -pbkdf2 \
    -pass fd:3 \
    3< <(echo -n "$PASS") \
    -in datos.txt -out datos.enc

Gestión de claves SSH

# Generar clave SSH con frase de protección
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519 -N "$FRASE"

# Usar agente SSH para no exponer la frase
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

# Script que usa SSH de forma segura
ejecucion_ssh_segura() {
    local host="$1"
    local comando="$2"

    # Validar host contra lista blanca
    case "$host" in
        server1.ejemplo.com|server2.ejemplo.com)
            ;;
        *)
            echo "ERROR: Host no autorizado: $host" >&2
            return 1
            ;;
    esac

    # Sanitizar el comando
    local cmd_seguro
    cmd_seguro=$(printf '%q' "$comando")

    # Ejecutar con timeout y opciones estrictas
    ssh -o StrictHostKeyChecking=yes \
        -o ConnectTimeout=10 \
        -o ServerAliveInterval=30 \
        -i ~/.ssh/deploy_key \
        "admin@$host" "$cmd_seguro"
}

Auditoría y logging (syslog)

Todo script que ejecute acciones sensibles debe registrar quién, qué y cuándo.

# Logging a syslog
logger -t mi_script -p user.info "Inicio de ejecución por $USER"
logger -t mi_script -p user.warning "Archivo no encontrado: $archivo"
logger -t mi_script -p user.err "Operación fallida: código $?"

# Enviar a archivo con formato estructurado
auditar() {
    local accion="$1"
    local resultado="$2"
    local detalles="${3:-}"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')

    # Formato: timestamp | usuario | acción | resultado | detalles
    echo "$timestamp | $USER | $accion | $resultado | $detalles" >> /var/log/mi_app/auditoria.log

    # También a syslog para SIEM centralizado
    logger -t mi_app -p user.notice "$USER | $accion | $resultado | $detalles"
}

# Uso
auditar "Eliminar usuario" "EXITO" "usuario=jdoe"

# Script completo de auditoría
#!/bin/bash
AUDIT_LOG="/var/log/mi_app/auditoria.log"
AUDIT_ENABLED=true

auditar() {
    $AUDIT_ENABLED || return 0
    local accion="$1"
    local resultado="$2"
    shift 2
    local detalles="$*"
    local ts
    ts=$(date --iso-8601=seconds)

    # PID, TTY, usuario real (SUDO_USER si existe)
    local usuario_real="${SUDO_USER:-$USER}"
    local tty=$(tty 2>/dev/null || echo "no-tty")

    printf "[%s] PID=$$ USER=%s TTY=%s ACCION=%s RESULTADO=%s DETALLES=%s\n" \
        "$ts" "$usuario_real" "$tty" "$accion" "$resultado" "$detalles" \
        | tee -a "$AUDIT_LOG" | logger -t mi_app -p user.notice
}

rm seguro — Protección contra borrados accidentales

# ❌ Clásico error
cd /directorio_importante
rm -rf *   # Si pwd falló, estás en / y borras TODO

# ✅ Verificar directorio antes
if [[ "$PWD" != "/directorio_importante" ]]; then
    echo "ERROR: No estás en el directorio correcto" >&2
    exit 1
fi
rm -rf ./*

# ✅ Usar trash-put (si está disponible)
if command -v trash-put &>/dev/null; then
    trash-put archivo_borrar
else
    rm -i archivo_borrar  # -i pide confirmación
fi

# ✅ rm seguro con confirmación y log
rm_seguro() {
    local archivo="$1"

    # Validar que el archivo existe
    if [[ ! -f "$archivo" && ! -d "$archivo" ]]; then
        echo "ERROR: No existe: $archivo" >&2
        return 1
    fi

    # Confirmar
    read -p "¿Borrar '$archivo' irreversiblemente? (s/N): " confirm
    if [[ "$confirm" != [sS] ]]; then
        echo "Operación cancelada"
        return 0
    fi

    # Registrar en log
    logger -t rm_seguro "Usuario $USER borró: $(realpath "$archivo")"

    # Borrar
    rm -rf "$archivo"
}

# ✅ Usar rm con --one-file-system (no cruza montajes)
rm -rf --one-file-system /tmp/datos_viejos/

# ✅ Mover a un directorio trash antes de borrar
TRASH_DIR="/tmp/.trash_$USER"
mkdir -p "$TRASH_DIR"
mv archivo_borrar "$TRASH_DIR/$(date +%s)_$(basename "$archivo")"
# Luego un cron se encarga de vaciar el trash periódicamente

Funciones profesionales

Función 1: sanitizar_input — Filtro de entrada universal

# Sanitiza una cadena eliminando caracteres peligrosos
# Uso: sanitizar_input <cadena> [tipo] [max_len]
#   tipo: alfanumerico | nombre | ruta | email | ip | comando
#   max_len: longitud máxima (defecto 256)
sanitizar_input() {
    local input="$1"
    local tipo="${2:-alfanumerico}"
    local max_len="${3:-256}"
    local limpio

    # Truncar
    limpio="${input:0:$max_len}"

    case "$tipo" in
        alfanumerico)
            limpio="${limpio//[^a-zA-Z0-9]/}"
            ;;
        nombre)
            # Letras, números, guiones, espacios, tildes
            limpio="${limpio//[^a-zA-Z0-9áéíóúÁÉÍÓÚüÜñÑ _-]/}"
            ;;
        ruta)
            # Solo caracteres de ruta Unix seguros
            limpio="${limpio//[^a-zA-Z0-9._-\/]/}"
            # Eliminar barras dobles
            while [[ "$limpio" == *"//"* ]]; do
                limpio="${limpio//\/\///}"
            done
            # Eliminar .. peligrosos
            limpio="${limpio//../}"
            ;;
        email)
            limpio="${limpio//[^a-zA-Z0-9.@_%+-]/}"
            ;;
        ip)
            limpio="${limpio//[^0-9.]/}"
            ;;
        comando)
            # Para construir comandos, escapar con printf %q
            printf '%q' "$limpio"
            return
            ;;
        *)
            echo "ERROR: Tipo de sanitización desconocido: $tipo" >&2
            return 1
            ;;
    esac

    echo "$limpio"
}

Función 2: tempfile_seguro — Archivo temporal con cleanup

# Crea un archivo temporal seguro con limpieza automática
# Uso: tempfile_seguro [prefijo] [directorio]
# Define: TEMP_FILE, TEMP_DIR
# Registra trap EXIT para limpieza
tempfile_seguro() {
    local prefijo="${1:-script}"
    local dir="${2:-/tmp}"
    local temp_file
    local temp_dir

    # Crear archivo temporal
    temp_file=$(mktemp "${dir}/${prefijo}.XXXXXX") || {
        echo "ERROR: No se pudo crear archivo temporal" >&2
        return 1
    }

    # Crear directorio temporal
    temp_dir=$(mktemp -d "${dir}/${prefijo}_dir.XXXXXX") || {
        rm -f "$temp_file"
        echo "ERROR: No se pudo crear directorio temporal" >&2
        return 1
    }

    # Exportar para que el trap los conozca
    TEMP_FILE="$temp_file"
    TEMP_DIR="$temp_dir"
    export TEMP_FILE TEMP_DIR

    # Registrar cleanup (solo una vez)
    if [[ -z "${_TEMP_CLEANUP_REGISTERED:-}" ]]; then
        _temp_cleanup() {
            [[ -f "$TEMP_FILE" ]] && rm -f "$TEMP_FILE"
            [[ -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
        }
        trap _temp_cleanup EXIT INT TERM
        export _TEMP_CLEANUP_REGISTERED=1
    fi

    echo "$temp_file"
}

Función 3: cifrar_backup — Cifrado seguro de archivos

# Cifra un archivo con AES-256 (OpenSSL o GPG)
# Uso: cifrar_backup <archivo_origen> [archivo_destino]
cifrar_backup() {
    local origen="$1"
    local destino="${2:-${origen}.enc}"
    local pass="${PASSWORD_CIFRADO:-}"
    local pass_file="${PASSFILE:-}"

    # Validar archivo origen
    if [[ ! -f "$origen" ]]; then
        echo "ERROR: Archivo origen no encontrado: $origen" >&2
        return 1
    fi

    # Obtener contraseña
    if [[ -z "$pass" && -n "$pass_file" && -f "$pass_file" ]]; then
        pass=$(< "$pass_file")
    fi

    if [[ -z "$pass" ]]; then
        read -s -p "🔑 Contraseña de cifrado: " pass
        echo >&2
    fi

    # Cifrar con OpenSSL
    if command -v openssl &>/dev/null; then
        openssl enc -aes-256-cbc -salt -pbkdf2 \
            -pass pass:"$pass" \
            -in "$origen" \
            -out "$destino" 2>/dev/null && {
            echo "✓ Archivo cifrado: $origen → $destino"
            # Limpiar variable de memoria (no perfecto pero ayuda)
            pass=""
            return 0
        }
    fi

    # Fallback a GPG
    if command -v gpg &>/dev/null; then
        gpg --symmetric --cipher-algo AES256 \
            --batch --passphrase "$pass" \
            -o "$destino" \
            "$origen" 2>/dev/null && {
            echo "✓ Archivo cifrado con GPG: $origen → $destino"
            pass=""
            return 0
        }
    fi

    echo "ERROR: No se pudo cifrar el archivo" >&2
    pass=""
    return 1
}

Función 4: auditar_evento — Registro de auditoría estructurado

# Registra un evento de auditoría en syslog y archivo
# Uso: auditar_evento <accion> <resultado> [detalles...]
# Variables de entorno: AUDIT_LOG (archivo de log opcional)
auditar_evento() {
    local accion="$1"
    local resultado="$2"
    shift 2
    local detalles="$*"
    local timestamp
    local usuario_real
    local tty_actual

    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    usuario_real="${SUDO_USER:-$USER}"
    tty_actual=$(tty 2>/dev/null || echo "?")
    local pid="$$"

    # Formato estructurado: TS | PID | USUARIO | TTY | ACCION | RESULTADO | DETALLES
    local mensaje="[$timestamp] PID=$pid USER=$usuario_real TTY=$tty_actual ACCION=$accion RESULTADO=$resultado DETALLES=$detalles"

    # Syslog
    logger -t script_audit -p user.notice "$mensaje"

    # Archivo (si se configuró)
    if [[ -n "${AUDIT_LOG:-}" ]]; then
        echo "$mensaje" >> "$AUDIT_LOG"
    fi

    # Stderr si es advertencia o error
    if [[ "$resultado" == "ERROR" || "$resultado" == "FALLO" ]]; then
        echo "⚠ $mensaje" >&2
    fi
}

Errores comunes

Error 1: No citar variables

# ❌ Explotable con inyección
archivo="$1"
cat $archivo
# Input: "archivo.txt ; echo HACKED"
# cat ejecuta "cat archivo.txt ; echo HACKED" → ¡dos comandos!

# ✅ Siempre citar
cat "$archivo"
# cat "archivo.txt ; echo HACKED" → un solo argumento (archivo no existe)

Error 2: Usar eval innecesariamente

# ❌ eval sobre datos externos
eval "echo $1"
# ./script.sh "$(rm -rf /)" → ¡desastre!

# ✅ printf %q o expansión directa
printf '%q\n' "$1"

# ✅ O simplemente
echo "$1"

Error 3: Ignorar el modo estricto

# ❌ Script sin protección
#!/bin/bash
# Si falla un comando, el script sigue como si nada

# ✅ Siempre poner al inicio
#!/bin/bash
set -euo pipefail

Error 4: Archivos temporales con nombre predecible

# ❌ Nombre fijo — ataque de symlink
echo "datos" > /tmp/mi_script.tmp
# Un atacante crea: ln -s /etc/passwd /tmp/mi_script.tmp
# ¡Sobreescribes /etc/passwd!

# ✅ mktemp con sufijo aleatorio
TEMP=$(mktemp /tmp/mi_script.XXXXXX)

Error 5: Scripts SUID

# ❌ Jamás
chmod u+s script.sh

# ✅ sudo con reglas específicas en /etc/sudoers
# usuario ALL=(root) /usr/local/bin/script.sh

Error 6: No limpiar trap en archivos temporales

# ❌ Si el script falla o recibe Ctrl+C, /tmp queda lleno
TEMP=$(mktemp)
procesar "$TEMP"
# Nunca se borra si falla

# ✅ Cleanup con trap
cleanup() { rm -f "$TEMP"; }
trap cleanup EXIT INT TERM
TEMP=$(mktemp)
procesar "$TEMP"

Error 7: Exponer contraseñas por argumento

# ❌ Visible en ps aux
./script.sh --password "supersecreto"

# ✅ Variable de entorno (mejor) o archivo con permisos 600
export DB_PASS="supersecreto"
./script.sh

Error 8: No validar entrada con regex

# ❌ Validación débil
if [[ "$1" == "admin" ]]; then
    echo "Acceso concedido"
fi

# ✅ Validación con regex y listas blancas
patron_admin='^admin[0-9]+$'
if [[ "$1" =~ $patron_admin ]]; then
    echo "Acceso concedido a $1"
fi

Error 9: No usar -- para delimitar opciones

# ❌ Si el archivo empieza con -, se interpreta como opción
rm $archivo
# archivo="-rf" → ¡rm interpreta -rf como opción!

# ✅ Usar -- para indicar fin de opciones
rm -- "$archivo"

# ✅ También aplica a otros comandos
cat -- "$archivo"
grep -- "$patron" "$archivo"

Error 10: Confiar en la integridad del PATH

# ❌ PATH manipulable — inyección de comandos
# Si PATH contiene /tmp, el atacante pone un falso "ls"
ls

# ✅ Usar rutas absolutas en scripts críticos
/bin/ls
/usr/bin/grep

# ✅ O establecer PATH seguro al inicio del script
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

Resumen

ConceptoHerramienta / SintaxisPropósito
Sanitizaciónprintf '%q' "$var"Escapar para shells
Validación regex[[ "$var" =~ ^patrón$ ]]Lista blanca de entrada
Eliminar caracteres${var//[^a-z]/}Filtrar caracteres no deseados
Modo estrictoset -euo pipefailFallar ante cualquier error
Citar variables"$var"Evitar word splitting y globbing
Archivos temporalesmktemp ...XXXXXXCrear archivos con nombres impredecibles
Cleanuptrap cleanup EXIT INT TERMLimpiar recursos siempre
umask seguroumask 0077Archivos privados por defecto
No eval"${comando[@]}" o printf %qEvitar ejecución de código arbitrario
Path traversalbasename, realpathValidar y resolver rutas
Sin SUIDsudo con reglas específicasEjecución privilegiada segura
Secretosread -s, .env, vars localesProteger contraseñas en scripts
Cifradoopenssl enc -aes-256-cbc -pbkdf2Cifrar datos sensibles
Auditoríalogger -t script -p user.noticeRegistrar eventos de seguridad
rm seguro--one-file-system, trash-putPrevenir borrados accidentales
Separar opciones--Indicar fin de opciones en comandos
PATH seguroexport PATH="/usr/bin:/bin"Prevenir inyección por PATH
Arrays"${arr[@]}"Construcción segura de comandos
Variables indirectas"${!nombre_var}"Alternativa a eval para vars dinámicas
GPG simétricogpg --symmetric --cipher-algo AES256Cifrado con contraseña

Más información,

Deja una respuesta