Los fallos de seguridad típicos de los scripts de Bash y cómo evitarlos

Seguridad en Bash: los doce fallos que más se repiten en los scripts, agrupados en cuatro bloques: las comillas y los nombres de archivo, la inyección de órdenes, el entorno, y los secretos y las descargas. Para cada uno, el patrón que hay que evitar, lo que pasa de verdad al ejecutarlo, cómo lo detecta ShellCheck y la forma segura, todo probado con Bash 5.3. Al final, la historia de Shellshock y una lista de comprobación.

Un script de Bash suele trabajar con más poder del que parece: corre con los permisos de quien lo lanza, a menudo root o una cuenta de servicio, muchas veces desde cron y sin nadie mirando, y maneja nombres de archivo, variables de entorno y argumentos que no controla. Por eso un descuido pequeño, una comilla que falta o una variable vacía, puede borrar lo que no debía o ejecutar órdenes que no escribió el autor. La buena noticia es que esos fallos se repiten, y casi todos los detecta ShellCheck, un analizador gratuito. Esta página reúne los más habituales con la misma estructura: qué es, el patrón que hay que evitar, lo que pasa de verdad (ejecutado con Bash 5.3 sobre archivos de prueba), cómo se detecta y la forma segura.

Cinco capas de defensa en los scripts de Bash: escribir con comillas, doble guion y arrays, sin eval; validar las entradas con listas blancas antes de usarlas; revisar con ShellCheck en cada cambio; ejecutar con set -euo pipefail y PATH y umask fijos; y mantener Bash al día con los permisos mínimosCinco capas: cada una atrapa lo que se escapó de la anteriorescribircomillas, — y arraysnada de evalvalidarlistas blancasantes de usarrevisarShellChecken cada cambioejecutarset -euo pipefailPATH y umask fijosmantenerBash al díay permisos mínimosNinguna basta sola: ShellCheck no ve los datos que llegan, y validar no sirve si falta una comilla.

Las defensas de esta página, ordenadas por el momento en que actúan. Esquema: decodigo.com.

Si estás aprendiendo, cada lección del curso de Bash tiene su recuadro «Ojo con la seguridad» con estos mismos fallos en contexto, y el proyecto final los evita todos. Las versiones de Bash que conviene tener, y la historia de Shellshock, están en Versiones y parches.

Comillas y nombres de archivo

1. Variables sin comillas

Es el fallo número uno, con diferencia. Cuando una variable se usa sin comillas, Bash hace dos cosas con su valor antes de pasarlo a la orden: lo divide en palabras por los espacios, tabuladores y saltos de línea, y trata cada palabra como un patrón de archivos (*, ?, […]). Un nombre con un espacio se convierte en dos argumentos, y una variable que contiene un asterisco se convierte en la lista de archivos de la carpeta.

El patrón que hay que evitar

#!/usr/bin/env bash
archivo="informe 2026.txt"
ls
rm $archivo
echo "--- después de rm \$archivo:"
ls

Lo que pasa de verdad

salida2026.txt
informe
informe 2026.txt
sin_comillas.sh
--- después de rm $archivo:
informe 2026.txt
sin_comillas.sh

rm recibió dos argumentos, informe y 2026.txt, y borró justo los dos archivos que no había que tocar; el que sí había que borrar sigue ahí. Con rm -rf y una ruta de carpeta, el mismo error puede llevarse por delante un directorio entero.

Cómo se detecta

shellcheckIn sin_comillas.sh line 4:
rm $archivo
   ^------^ SC2086 (info): Double quote to prevent globbing and word splitting.

Did you mean:
rm "$archivo"

For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...

La forma segura

Comillas dobles alrededor de toda expansión: "$var", "$(orden)", "${lista[@]}". Y -- antes de los nombres, por si alguno empieza por guion (el siguiente fallo):

#!/usr/bin/env bash
archivo="informe 2026.txt"
ls
rm -- "$archivo"
echo "--- después de rm -- \"\$archivo\":"
ls
salida2026.txt
informe
informe 2026.txt
sin_comillas_ok.sh
--- después de rm -- "$archivo":
2026.txt
informe
sin_comillas_ok.sh

Las únicas excepciones razonables son dentro de [[ … ]] y en una asignación simple (a=$b), donde Bash no divide, pero ponerlas tampoco hace daño.

2. Nombres de archivo que empiezan por guion

Un archivo puede llamarse -n, -rf o --help. Cuando un patrón como * se expande, esos nombres llegan a la orden como si fueran opciones. Aquí, una carpeta notas/ con lista.txt y un archivo vacío llamado -n:

El patrón que hay que evitar

#!/usr/bin/env bash
# Enseña todos los archivos de la carpeta notas/.
cd notas || exit 1
cat *

Lo que pasa de verdad

salida     1	pan
     2	leche

cat recibió -n lista.txt y numeró las líneas: -n dejó de ser un archivo para ser una opción. Con cat es inofensivo; con rm * y un archivo llamado -rf en la carpeta, no tanto.

Cómo se detecta

shellcheckIn guion.sh line 4:
cat *
    ^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options.

For more information:
  https://www.shellcheck.net/wiki/SC2035 -- Use ./*glob* or -- *glob* so name...

La forma segura

Hay dos formas, y conviene usar las dos: --, que casi todas las órdenes entienden como «se acabaron las opciones», y patrones con ruta, ./*, cuyos resultados empiezan por ./ y nunca por guion:

#!/usr/bin/env bash
# Enseña todos los archivos de la carpeta notas/: ./* nunca empieza por guion.
cd notas || exit 1
for f in ./*; do
    [[ -s $f ]] && cat -- "$f"
done
salidapan
leche

3. find y xargs con nombres raros

xargs separa lo que lee por espacios y saltos de línea, y además interpreta las comillas. Un nombre con un espacio, o con un salto de línea, que en Linux también es válido, se parte en trozos:

El patrón que hay que evitar

#!/usr/bin/env bash
find . -name '*.txt' | xargs wc -l

Lo que pasa de verdad

salida con error1 ./c.txt
wc: ./a: No such file or directory
wc: b.txt: No such file or directory
1 total
código de salida: 123

Cómo se detecta

shellcheckIn xargs.sh line 2:
find . -name '*.txt' | xargs wc -l
^------------------^ SC2038 (warning): Use 'find .. -print0 | xargs -0 ..' or 'find .. -exec .. +' to allow non-alphanumeric filenames.

For more information:
  https://www.shellcheck.net/wiki/SC2038 -- Use 'find .. -print0 | xargs -0 ....

La forma segura

Separar los nombres con el carácter nulo, el único que no puede aparecer en un nombre de archivo: find -print0 con xargs -0 (y sort -z si hace falta ordenarlos). O dejar que find llame a la orden por su cuenta con -exec … {} +:

#!/usr/bin/env bash
find . -name '*.txt' -print0 | sort -z | xargs -0 wc -l
salida 2 ./a b.txt
 1 ./c.txt
 3 total

4. rm -rf con una variable vacía

El fallo con más fama, porque cuando pasa es catastrófico. Una variable que debía tener una ruta llega vacía, porque no se leyó la configuración, por una errata en el nombre o porque una orden falló, y "$DIR/"* se convierte en /*. En enero de 2015, un usuario del cliente de Steam para Linux informó de que, tras mover la carpeta de Steam, el programa borró todos los archivos de su usuario, incluido un disco de copias de seguridad; en el hilo se señaló una línea de steam.sh con exactamente este patrón, rm -rf "$STEAMROOT/"*, con STEAMROOT vacía.

El patrón que hay que evitar

#!/usr/bin/env bash
# limpiar-copias.sh: NUNCA ejecutes este script. Solo se pasa por ShellCheck.
DIR_COPIAS=$(cat ~/.config/copias/ruta 2>/dev/null)
rm -rf "$DIR_COPIAS/"*

Lo que pasa de verdad

Este script no se ejecuta en ningún sitio, ni siquiera en el contenedor de pruebas. Escribir la orden con echo en lugar de ejecutarla enseña lo que habría corrido:

#!/usr/bin/env bash
# La misma orden, pero solo escrita: enseña lo que se habría ejecutado, sin borrar nada.
DIR_COPIAS=$(cat ~/.config/copias/ruta 2>/dev/null)   # el archivo no existe: la variable queda vacía
echo "Se ejecutaría: rm -rf $DIR_COPIAS/*"
salidaSe ejecutaría: rm -rf /*

La carpeta de configuración no existía, DIR_COPIAS quedó vacía y /* son todas las carpetas del sistema: la orden habría intentado borrarlo todo. Como usuario normal, rm fallaría con casi todo lo de fuera de tu carpeta personal, pero con todo lo tuyo, no; y si el script corre como root, no hay nada que lo pare.

Cómo se detecta

shellcheckIn rm_peligro.sh line 4:
rm -rf "$DIR_COPIAS/"*
       ^-------------^ SC2115 (warning): Use "${var:?}" to ensure this never expands to /* .

For more information:
  https://www.shellcheck.net/wiki/SC2115 -- Use "${var:?}" to ensure this nev...

La forma segura

${VAR:?mensaje} detiene el script con un error si la variable está vacía o sin definir, antes de que la orden llegue a ejecutarse. Junto con set -u, que convierte en error usar una variable que no existe (y así caza las erratas en los nombres), y comprobando que las órdenes de las que sale la ruta terminaron bien:

#!/usr/bin/env bash
set -euo pipefail
DIR_COPIAS=$(cat ~/.config/copias/ruta 2>/dev/null || true)
echo "Voy a vaciar ${DIR_COPIAS:?no sé qué carpeta vaciar: falta ~/.config/copias/ruta}"
rm -rf -- "${DIR_COPIAS:?}"/*
salida con error./rm_ok.sh: line 4: DIR_COPIAS: no sé qué carpeta vaciar: falta ~/.config/copias/ruta
código de salida: 1

Inyección de órdenes

5. eval, bash -c y ssh con datos de fuera

eval toma un texto y lo ejecuta como código de Bash. Si en ese texto hay algo que viene de fuera, un argumento, un nombre de archivo, una línea de un formulario, quien escribe ese dato puede añadir sus propias órdenes. Lo mismo pasa con todo lo que pasa una cadena a otra shell: bash -c "…", sh -c, su -c o ssh servidor "…", que ejecuta la cadena en la shell del otro equipo.

El patrón que hay que evitar

#!/usr/bin/env bash
# saludar.sh NOMBRE: arma la orden como texto y la ejecuta con eval.
nombre=$1
eval "echo Hola, $nombre"

Lo que pasa de verdad

Llamado con un nombre que lleva un punto y coma, el script ejecuta una segunda orden que no escribió su autor (aquí, un echo inofensivo; podría ser cualquier cosa):

./saludar.sh 'Ana; echo esta orden la escribió quien llamó al script'
salidaHola, Ana
esta orden la escribió quien llamó al script

Cómo se detecta

ShellCheck no avisa de este eval: para él es una orden legítima, y no puede saber de dónde viene la variable. Hay que buscarlo a mano: grep -nE '\beval\b|bash -c|sh -c|ssh ' *.sh, y revisar cada resultado preguntándose si algún dato de fuera llega a la cadena.

La forma segura

No construir código con datos. Para mostrar o usar un dato, comillas y printf; para una orden con argumentos variables, un array (orden=(grep -- "$patron" "$archivo") y "${orden[@]}"). Y si no queda más remedio que pasar una cadena a otra shell, escapar cada dato con printf '%q', que lo deja como un único argumento literal:

#!/usr/bin/env bash
# saludar.sh NOMBRE: el dato nunca se interpreta como código.
nombre=$1
printf 'Hola, %s\n' "$nombre"

# Si de verdad hay que pasar una orden a otra shell (bash -c, ssh, su -c), se escapa con %q:
printf -v seguro '%q' "$nombre"
bash -c "echo Hola otra vez, $seguro"
salidaHola, Ana; echo esta orden la escribió quien llamó al script
Hola otra vez, Ana; echo esta orden la escribió quien llamó al script

6. Inyección en las cuentas

Un fallo menos conocido, y por eso más peligroso. Dentro de (( … )), $(( … )), let y las comparaciones numéricas de [[ … -gt … ]], Bash evalúa los nombres de variable que encuentra, y un nombre con un índice de array, x[…], se evalúa entero, incluida una sustitución de órdenes dentro del índice. Si el «número» viene de fuera, puede traer código:

El patrón que hay que evitar

#!/usr/bin/env bash
# limite.sh N: comprueba un número que llega de fuera.
n=$1
if (( n > 100 )); then
    echo "demasiado grande"
else
    echo "vale: $n"
fi

Lo que pasa de verdad

salidaesta orden se coló en una comparación
vale: x[$(echo esta orden se coló en una comparación >&2)]

La primera línea la escribió la orden escondida en el «número», al evaluar la comparación. El script ni siquiera falla: sigue como si nada.

Cómo se detecta

ShellCheck no lo detecta: (( n > 100 )) es perfectamente correcto si n es un número. La defensa está en no meter en una cuenta nada que no se haya validado antes.

La forma segura

Comprobar con una expresión regular que el dato es solo cifras, y solo entonces usarlo. El 10# evita otra sorpresa: sin él, un número con un cero delante, como 010, se lee en octal.

#!/usr/bin/env bash
# limite.sh N: primero se comprueba que es un número; solo entonces se usa en una cuenta.
n=$1
if [[ ! $n =~ ^[0-9]{1,9}$ ]]; then
    echo "limite.sh: «$n» no es un número entero positivo" >&2
    exit 1
fi
if (( 10#$n > 100 )); then
    echo "demasiado grande"
else
    echo "vale: $n"
fi
salida con errorlimite.sh: «x[$(echo esta orden se coló en una comparación >&2)]» no es un número entero positivo
código de salida: 1

7. Rutas con ../

Cuando un nombre de archivo que llega de fuera se pega a una carpeta, "informes/$1", quien elige el nombre puede salir de ella con ../. Es el clásico «path traversal» de las aplicaciones web, y en los scripts aparece cada vez que uno sirve archivos a otros, desde una tarea de CI hasta un script detrás de un servidor web.

El patrón que hay que evitar

#!/usr/bin/env bash
# ver-informe.sh NOMBRE: enseña un informe de la carpeta informes/.
cat "informes/$1"

Lo que pasa de verdad

./ver-informe.sh ../secreto.txt
salidacontraseña de prueba: no-es-real

Cómo se detecta

ShellCheck no puede saberlo: la orden está bien escrita. Hay que preguntarse, en cada ruta que se construye con un dato de fuera, qué pasa si el dato lleva /, .. o empieza por /.

La forma segura

Una lista blanca: aceptar solo los nombres que tienen la forma esperada (letras, cifras, guiones y la extensión) y rechazar todo lo demás, en vez de intentar quitar los ../, que siempre deja algún caso fuera. Y, como segunda línea, resolver la ruta final con realpath y comprobar que sigue dentro de la carpeta:

#!/usr/bin/env bash
# ver-informe.sh NOMBRE: solo nombres sencillos, y la ruta final tiene que quedar dentro de informes/.
set -euo pipefail
nombre=$1
if [[ ! $nombre =~ ^[A-Za-z0-9_-]+\.txt$ ]]; then
    echo "ver-informe.sh: nombre no válido: $nombre" >&2
    exit 1
fi
base=$(realpath -e informes)
ruta=$(realpath -e -- "informes/$nombre") || exit 1
[[ $ruta == "$base"/* ]] || { echo "ver-informe.sh: fuera de informes/" >&2; exit 1; }
cat -- "$ruta"
terminal$ bash ver-informe.sh enero.txt
Enero: 1,250 visitas
$ bash ver-informe.sh ../secreto.txt; echo "código: $?"
ver-informe.sh: nombre no válido: ../secreto.txt
código: 1
$ bash ver-informe.sh 'enero.txt;id'; echo "código: $?"
ver-informe.sh: nombre no válido: enero.txt;id
código: 1

El entorno

8. Un PATH que no controlas

Cuando un script llama a ls, cp o tar sin ruta, Bash busca la orden en las carpetas de PATH, en orden. Si quien prepara el entorno puede poner delante una carpeta suya, el script ejecutará su programa en lugar del del sistema. Importa sobre todo en los scripts que se ejecutan con más permisos que quien los lanza, o desde servicios que heredan el entorno de otro.

El patrón que hay que evitar

#!/usr/bin/env bash
# limpieza.sh: usa ls sin ruta, así que ejecuta el primer «ls» que encuentre en el PATH.
echo "Carpeta de temporales:"
ls -d /tmp

Lo que pasa de verdad

En la carpeta bin/ hay un ls falso que solo escribe un aviso. Con esa carpeta delante en el PATH:

terminal$ chmod +x bin/ls
$ PATH="$PWD/bin:$PATH" bash limpieza.sh
Carpeta de temporales:
[este no es el ls del sistema: es /home/ana/proyecto/bin/ls]
$ PATH="$PWD/bin:$PATH" bash limpieza-segura.sh
Carpeta de temporales:
/tmp

La forma segura

Los scripts que corren con privilegios fijan su propio PATH al principio, con solo carpetas del sistema que nadie más puede escribir, como hace limpieza-segura.sh:

#!/usr/bin/env bash
# limpieza.sh: lo primero, un PATH fijo con solo carpetas del sistema.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
echo "Carpeta de temporales:"
ls -d /tmp

sudo ya hace algo parecido por su cuenta: sustituye el PATH por el de su opción secure_path. Lo mismo vale para otras variables que cambian el comportamiento de las órdenes, como LD_PRELOAD, IFS o BASH_ENV: un script privilegiado no debe fiarse del entorno que hereda.

9. Archivos temporales predecibles

Un nombre fijo como /tmp/informe.tmp es un problema en una carpeta que comparten todos los usuarios: otro usuario puede crear antes ese archivo, o un enlace simbólico con ese nombre que apunte a un archivo tuyo, y el script escribirá donde él decida. Es una «condición de carrera»: entre que el script comprueba si el archivo existe y lo usa, otro puede cambiarlo.

El patrón que hay que evitar

tmp=/tmp/informe.tmp
[[ -e $tmp ]] || ordenar_datos > "$tmp"     # entre la comprobación y la escritura cabe otro proceso

La forma segura

mktemp crea el archivo con un nombre impredecible y permisos solo para ti, en una sola operación que no deja hueco a nadie; trap … EXIT lo borra al terminar, pase lo que pase. set -o noclobber hace que > se niegue a pisar un archivo que ya existe, y umask 077 hace que todo lo que cree el script sea privado:

terminal$ tmp=$(mktemp)
$ trap 'rm -f -- "$tmp"' EXIT
$ stat -c '%A %U' "$tmp"
-rw------- ana
$ [[ $tmp == /tmp/tmp.* ]] && echo "nombre impredecible: /tmp/tmp. y diez caracteres al azar"
nombre impredecible: /tmp/tmp. y diez caracteres al azar
$ set -o noclobber
$ echo "nuevo contenido" > ya-existe.txt
bash: ya-existe.txt: cannot overwrite existing file
$ cat ya-existe.txt
datos importantes
$ umask 077; touch nuevo.txt; stat -c '%A %n' nuevo.txt
-rw------- nuevo.txt

10. Scripts con sudo o setuid

En Linux, el bit setuid no sirve en los scripts: el kernel lo ignora en los archivos que empiezan por #!, precisamente porque es imposible hacerlos seguros (la página de manual de execve lo dice así: «Linux ignora los bits set-user-ID y set-group-ID en los scripts»). La forma correcta de dejar que un usuario ejecute un script con permisos de root es una regla de sudo, y esa regla tiene sus propias trampas:

  • El script y la carpeta que lo contiene tienen que ser de root y no escribibles por nadie más; si el usuario puede editarlo, puede ejecutar lo que quiera como root.
  • La regla en /etc/sudoers.d/ debe nombrar la ruta completa del script y, si se puede, los argumentos permitidos; un comodín como /opt/scripts/* deja pasar argumentos que nadie revisó.
  • El script debe tratar sus argumentos como datos de un desconocido: todo lo de esta página aplica, con root.
  • Nunca dar con sudo órdenes que permiten escapar a una shell, como editores, less, find con -exec o un intérprete.

Secretos y descargas

11. Contraseñas en los argumentos, el entorno o el historial

Lo que va en la línea de órdenes lo puede ver cualquier usuario del equipo mientras el proceso corre, con ps. Las variables de entorno no son visibles para otros usuarios, pero sí para cualquier proceso tuyo, para los programas que lances y, a menudo, para los registros de error que vuelcan el entorno. Y lo que se teclea en la terminal acaba en ~/.bash_history.

terminal$ bash -c 'sleep 1; true' cliente --clave=prueba-123 &
$ sleep 0.3; ps -o args= -p $!
bash -c sleep 1; true cliente --clave=prueba-123
$ wait
$ CLAVE=prueba-456 sleep 1 &
$ sleep 0.3; stat -c '%A' /proc/$!/environ; tr '\0' '\n' < /proc/$!/environ | grep CLAVE
-r--------
CLAVE=prueba-456
$ wait

La clave del argumento la habría visto cualquiera con ps. La del entorno solo es legible por el mismo usuario (los permisos de /proc/…/environ son -r--------), pero la heredan todos los procesos hijos.

La forma segura

Leer los secretos de un archivo con permisos solo para el dueño (y comprobarlos), o de la entrada estándar; con read -rs si se piden por teclado, para que no se vean al escribirlos. Y pasarlos al programa que los necesita por la entrada estándar o por un archivo, no como argumento:

terminal$ chmod 600 clave.txt
$ [[ $(stat -c %a clave.txt) == 600 ]] && echo "permisos correctos: solo el dueño"
permisos correctos: solo el dueño
$ IFS= read -r clave < clave.txt
$ echo "La clave tiene ${#clave} caracteres; no hace falta enseñarla."
La clave tiene 10 caracteres; no hace falta enseñarla.

Para el historial, con HISTCONTROL=ignorespace en tu .bashrc, las órdenes que empiezan por un espacio no se guardan; y si se te escapó una, history -d borra esa línea.

12. curl | bash y las descargas sin comprobar

Muchas instrucciones de instalación piden copiar algo como curl -fsSL https://… | bash. Eso ejecuta, con tus permisos y a ciegas, lo que devuelva ese servidor en ese momento: si el sitio está comprometido, si alguien manipula la conexión o si la descarga se corta a mitad (y se ejecuta media orden), no te enterarás. No es un fallo de Bash, sino de confianza, pero es la forma más común de ejecutar código ajeno en un servidor.

La forma segura

Descargar primero, leerlo, comprobar su suma o su firma contra la que publica el autor por otro canal y solo entonces ejecutarlo. La comprobación de la suma con sha256sum -c detecta cualquier cambio, aunque sea de un solo carácter:

terminal$ cat instalar.sh.sha256
68e18cbd612a287528e05776d985f634894fc157e6088650b3c30bd811aa398a  instalar.sh
$ sha256sum -c instalar.sh.sha256
instalar.sh: OK
$ echo '# una línea añadida por el camino' >> instalar.sh
$ sha256sum -c instalar.sh.sha256; echo "código: $?"
instalar.sh: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
código: 1

Una suma solo protege si viene de un sitio distinto del archivo (si el atacante cambia los dos, coinciden); por eso los proyectos serios firman sus versiones con GPG o Sigstore, y lo más sólido es instalar desde los repositorios de la distribución, que verifican las firmas por ti.

Shellshock: el día que el fallo estaba en Bash

Todo lo anterior son fallos de los scripts. En septiembre de 2014 el fallo estaba en el propio Bash. Shellshock (CVE-2014-6271, publicada el 24 de septiembre de 2014) afectaba a todas las versiones hasta la 4.3: Bash procesaba lo que venía después de la definición de una función guardada en una variable de entorno, y lo ejecutaba al arrancar. Como muchos servicios pasan datos del exterior a Bash a través de variables de entorno (los CGI de Apache, los clientes DHCP o la opción ForceCommand de OpenSSH, según la descripción oficial), un atacante podía ejecutar órdenes en servidores que ni siquiera tenían scripts de Bash a la vista. El primer parche resultó incompleto (CVE-2014-7169, al día siguiente) y llegaron varias correcciones más en pocos días. CISA las incluyó en 2022 en su catálogo de vulnerabilidades explotadas, ocho años después.

Cualquier Bash de los últimos diez años está corregido, pero aún aparecen versiones viejas en aparatos que nadie actualiza: enrutadores, cámaras, sistemas embebidos y servidores olvidados. Comprobar la versión es lo primero:

terminal$ bash --version | head -1
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)
$ echo "$BASH_VERSION"
5.3.9(1)-release

La lección de Shellshock vale para todo lo de esta página: los datos que llegan de fuera, incluidas las variables de entorno, nunca deben acabar interpretados como código.

Cómo revisar un script que ya existe

  1. Pásalo por ShellCheck: shellcheck -x script.sh (o todos a la vez: find . -name '*.sh' -print0 | xargs -0 shellcheck -x). Corrige primero los avisos SC2086 (comillas), SC2115 (rm con variables) y SC2035/SC2038 (nombres raros), y no desactives un aviso sin una línea que explique por qué.
  2. Ponlo en la integración continua: ShellCheck devuelve un código distinto de cero si encuentra algo, así que basta con añadirlo como un paso más que haga fallar la revisión.
  3. Busca a mano lo que ShellCheck no ve: eval, bash -c, ssh con cadenas, cuentas con datos de fuera y rutas construidas con argumentos.
  4. Revisa dónde corre y con qué permisos: si lo lanza cron, sudo o un servicio, fija PATH y umask, y asegúrate de que nadie más puede escribir el script ni su carpeta.
  5. Ensáyalo con nombres hostiles: espacios, guiones al principio, asteriscos, comillas y saltos de línea en los nombres de archivo de prueba, y variables vacías.

Lista de comprobación

  • Comprobación
  • ☐Todas las expansiones entre comillas dobles: "$var", "$(orden)", "${lista[@]}"
  • ☐-- antes de los nombres de archivo y patrones con ruta (./*)
  • ☐find -print0 | xargs -0 o find -exec … {} +
  • ☐set -u y ${VAR:?} en toda ruta que se borra o se sobrescribe
  • ☐Ningún eval ni bash -c con datos de fuera; arrays para las órdenes y printf %q si no queda otra
  • ☐Todo dato de fuera se valida con una lista blanca antes de usarlo en cuentas o rutas
  • ☐PATH y umask fijos en los scripts que corren con privilegios; script y carpeta solo escribibles por su dueño
  • ☐Temporales con mktemp y trap … EXIT
  • ☐Secretos en archivos 600 o por la entrada estándar, nunca en argumentos
  • ☐Descargas comprobadas con suma o firma antes de ejecutarlas
  • ☐ShellCheck sin avisos, también en la integración continua
  • ☐Bash al día: bash --version

Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red y con archivos de prueba, y se revisaron con ShellCheck 0.11; las salidas y los avisos están copiados de esa ejecución. El script con rm -rf peligroso solo se pasó por ShellCheck, nunca se ejecutó. Esta página explica los fallos para detectarlos y corregirlos, no cómo aprovecharlos.