Guia de tecnologias

Boas práticas de segurança e robustez para scripts Bash

11 min read Shell Atualizado 22 Oct 2025
Práticas seguras para scripts Bash
Práticas seguras para scripts Bash

Importante: este artigo assume Bash moderno (versão 4+ quando aplicável). Se trabalhar com shells restritos (dash, sh) verifique compatibilidade.

Sumário

  • Introdução e filosofia
  • Shebang: qual escolher e por quê
  • Citações e expansão de variáveis
  • Modo estrito e controle de erros
  • Verificação explícita de falhas e tratamento
  • Depuração com xtrace e boas práticas de logging
  • Parâmetros longos e legibilidade de comandos
  • Substituição de comandos moderna ($())
  • Valores padrão e aninhamento
  • Duplo hífen (–) para separar opções de argumentos
  • Variáveis locais em funções e arrays
  • Checklist de qualidade e critérios de aceitação
  • Padrões e anti-padrões com exemplos
  • Segurança, permissões e hardening
  • Ferramentas de validação e testes
  • Modelos e snippets práticos
  • Resumo final

Introdução e filosofia

Bash é onipresente em sistemas UNIX-like. Com ele fazemos automações, instaladores, rotinas de manutenção e integrações. Por outro lado, erros em scripts podem gerar perda de dados, escalonamento de permissões ou downtime. A filosofia aqui é simples:

  • Programar defensivamente: assumir que qualquer entrada pode ser malformada.
  • Falhar rápido: detectar e interromper quando algo anômalo ocorre.
  • Ser explícito: prefira clareza sobre concisão obscura.
  • Testar cedo: validar em ambientes não produtivos.

Definições rápidas:

  • Shebang: a primeira linha do script que indica o interpretador (por exemplo, #!/usr/bin/env bash).
  • Substituição de comando: executar um comando e capturar sua saída (ex.: VAR=$(ls)).
  • Set strict (ou modo estrito): combinação de opções do shell que torne o script mais rígido e previsível.

1. Use uma boa linha shebang

A primeira linha do seu script deve declarar o interpretador. Isso evita comportamentos ambíguos quando alguém executar o arquivo diretamente.

Opções comuns:

  • Caminho fixo: #!/bin/bash — garante execução com o bash localizado exatamente em /bin/bash.
  • Via env: #!/usr/bin/env bash — resolve o bash a partir do PATH do ambiente do usuário.

Comparação rápida:

AbordagemVantagemRisco/Contras

| #!/bin/bash | Determinístico; garante a versão instalada naquele caminho | Menos portátil em sistemas onde bash fica noutro lugar ou é gerenciado por usuários | #!/usr/bin/env bash | Portátil; respeita PATH do usuário | Se PATH for malicioso, executável errado pode ser usado

Quando escolher cada uma:

  • Para scripts de uso pessoal ou em ambientes controlados (servidores com configuração fixa): #!/bin/bash pode ser apropriado.
  • Para scripts que serão distribuídos e executados em máquinas variadas (colaboradores, contêineres, diferentes distros): #!/usr/bin/env bash tende a ser mais portátil.

Fluxo de decisão (ajuda visual):

flowchart TD
  A[Precisa garantir versão exata do bash?] -->|Sim| B[#/bin/bash]
  A -->|Não| C[Está distribuindo para diversos ambientes?]
  C -->|Sim| D[#/usr/bin/env bash]
  C -->|Não| B

Notas de segurança:

  • Em ambientes restritos (produção sensível), defina a versão esperada no deploy e audite PATH.
  • Considere usar hashes de integridade e assinaturas quando distribuir scripts críticos.

2. Sempre coloque variáveis entre aspas

Espaços e quebras de linha em nomes de ficheiros são uma fonte clássica de bugs. Ao expandir variáveis nunca use a expansão não-quotada, a menos que tenha um motivo explícito.

Exemplo perigoso:

#!/bin/bash

FILENAME="docs/Letter to bank.doc"
ls $FILENAME

A expansão se torna: ls docs/Letter to bank.doc — o shell trata isso como três argumentos.

Correção segura:

ls "$FILENAME"

Use chaves quando for concatenar com texto literal:

echo "_${FILENAME}_ é um dos meus ficheiros favoritos"

Por que as chaves ajudam? Elas delimitam o nome da variável para evitar ambiguidades, por exemplo ${VAR}s vs $VARs.

Edge cases e arrays:

  • Para arrays, cite índices e expansões adequadas: “${array[@]}” preserva elementos.
  • Nunca use “${array[*]}” sem entender que ele junta os elementos em uma única string.

3. Pare o script em erro: modo estrito

Uma rede de verificações manuais é boa, mas há um atalho poderoso: ativar opções que fazem o shell interromper ao primeiro erro relevante.

Combinações úteis e frequentemente recomendadas (conhecida como “modo estrito”):

set -euo pipefail
IFS=$'\n\t'

Significados:

  • set -e: sai imediatamente se qualquer comando retornar status != 0 (com nuances; ver notas abaixo).
  • -u: trata variáveis não definidas como erro (nounset).
  • -o pipefail: em pipelines, retorna o status do primeiro comando que falhar.
  • IFS alterado para newline+tab reduz problemas com campos separados por espaços.

Observações importantes sobre set -e:

  • Nem sempre funciona como esperado com comandos em subshells, &&/|| e dentro de testes condicionais. Por isso, ainda é necessário validar retornos quando o fluxo exige.
  • Use traps para limpeza quando sair inesperadamente (ex.: remover ficheiros temporários).

Exemplo com cleanup:

#!/usr/bin/env bash
set -euo pipefail
trap 'rc=$?; echo "Saindo com $rc"; cleanup; exit $rc' EXIT

tmpfile=$(mktemp)
# ... usar $tmpfile ...

4. Trate falhas específicas e propague erro: pague adiante

Falhar por defeito (fail fast) protege, mas você também deve tratar exceções quando faz sentido.

Exemplo tradicional e mais verboso:

cd "$DIR"
if [ $? -ne 0 ]; then
  echo "Falha ao entrar em $DIR" >&2
  exit 1
fi

Formas idiomáticas e concisas:

cd "$DIR" || { echo "Falha ao entrar em $DIR" >&2; exit 1; }

Ou com função de erro:

fail() { echo "$*" >&2; exit 1; }
cd "$DIR" || fail "Não foi possível acessar $DIR"

Quando capturar saída e ainda assim seguir:

  • Capture e valide: out=$(cmd) || fail “cmd falhou”
  • Para comandos que podem falhar, documente os casos admitidos claramente.

5. Depure cada comando com xtrace e logging significante

Ative rastreamento para ver o que o script realmente executa:

set -o xtrace  # ou set -x

Isso imprime cada linha (após expansão) antes de executar. Útil em CI, logs e troubleshooting.

Boas práticas de logging:

  • Use printf em vez de echo quando precisar de previsibilidade de escape.
  • Logue para stderr (>&2) as mensagens de erro.
  • Forneça prefixos e timestamps quando apropriado: printf ‘%s %s\n’ “$(date -u +%Y-%m-%dT%H:%M:%SZ)” “[ERROR] Mensagem” >&2

Exemplo de debug seletivo:

if [ "${DEBUG:-}" = "1" ]; then
  set -x
fi
# ... script ...
set +x

Imagem de exemplo:

Saída de script com set -o xtrace mostrando comandos date e ls.


6. Prefira parâmetros longos ao chamar comandos

Opções curtas (-r -f) são ambíguas para leitores ocasionalmente. Em scripts, prefira a forma longa quando disponível.

Exemplo legível:

rm --recursive --force filename

Observações:

  • Nem todas as ferramentas têm formas longas. Verifique a –help ou man.
  • Para interoperabilidade, garanta compatibilidade ao usar opções longas em ambientes antigos.

7. Use a forma moderna de substituição de comando

Prefira $(…) em vez de ... (crase/backticks). A sintaxe moderna lida melhor com aninhamento e legibilidade.

Exemplo:

VAR=$(ls)
VAR2=$(ls -1)

Evite usar backticks — eles são legados e mais propensos a erros.


8. Declare valores padrão de forma concisa

Use a expansão com :- para ter valores padrão sem ifs adicionais.

CMD=${PAGER:-more}
DIR=${1:-${HOME:-/home/usuario}}

Diferença entre :- e -:

  • ${var:-default}: se var estiver vazio ou unset, usa default.
  • ${var-default}: se var for unset, usa default; se estiver definido mas vazio, usa o valor vazio.

Escolha consciente conforme a semântica desejada.


9. Use duplo hífen (–) para separar opções

Ficheiros que começam por - confundem comandos que interpretam opções.

Exemplo perigoso:

# Se existir um ficheiro chamado -rf, este comando pode ser catastrófico:
rm *

Proteção simples:

rm -- *.md

Também pode usar caminhos explícitos (./-nome) para indicar arquivo atual:

rm ./-nome

Imagem ilustrativa de criação de ficheiro com hífen e problema:

Terminal criando um ficheiro com hífen inicial e listando-o.

Erro típico:

Erro do ls dizendo 'opção não reconhecida' ao processar nome de ficheiro começando por hífen.


10. Use variáveis locais em funções e arrays

Por padrão, variáveis em Bash são globais. Dentro de funções, declare locais para evitar efeitos colaterais.

Exemplo inseguro:

#!/bin/bash
function run {
  DIR=$(pwd)
  echo "fazendo algo..."
}

DIR="/usr/local/bin"
run
echo $DIR

Saída pode mostrar DIR modificado. Correção:

function run {
  local DIR=$(pwd)
  echo "fazendo algo..."
}

Trabalhe com arrays quando precisar de coleções de argumentos:

files=("file one.txt" "file two.txt")
for f in "${files[@]}"; do
  printf 'Processando: %s\n' "$f"
done

11. Checklist de qualidade para um script robusto

Use esta lista como um SOP mínimo antes de colocar um script em produção.

  • Shebang apropriado (#/usr/bin/env bash ou /bin/bash conforme necessidade).
  • set -euo pipefail (ou uma variante documentada) e trap para cleanup.
  • Todas as expansões de variáveis são citadas: “$VAR”.
  • Uso de chaves quando concatenar: “${VAR}text”.
  • Não há dependência de IFS padrão sem justificativa.
  • Entrada de utilizador lida com read -r para preservar barras e espaços.
  • Uso de arrays para listas de ficheiros/argumentos.
  • Evitar parsing de ls; usar find, stat ou arrays.
  • Testes unitários e de integração (ex.: bats) para caminhos críticos.
  • Revisão com shellcheck e uma revisão por pares.
  • Logging informativo e mensagens de erro para stderr.

Critérios de aceitação — o script é robusto se atender aos itens acima e passar os testes automatizados.


12. Padrões e anti-padrões (com exemplos)

Anti-padrão: iterar sobre ls para obter ficheiros

for f in $(ls *.txt); do
  echo "$f"
done

Problema: espaços em nomes quebram o loop. Correto:

shopt -s nullglob
for f in *.txt; do
  printf '%s\n' "$f"
done

Ou usando arrays:

mapfile -t files < <(printf '%s\0' *.txt)
# melhor: usar find -print0 + read -d '\0' quando recursivo

Anti-padrão: usar echo para strings arbitrárias (problemas com escapes)

Use printf:

printf '%s\n' "Variável com\nquebras"

13. Segurança e hardening (práticas essenciais)

  • Minimize privilégios: não execute scripts como root a menos que estritamente necessário.
  • Evite confiar no PATH — use caminhos absolutos para binários críticos quando segurança for vital.
  • Sanitize entradas que acabarão em comandos (onde possível, evite construções via eval ou concatenação insegura).
  • Use mktemp para ficheiros temporários e garanta permissões restritas: mktemp -p /tmp myscript.XXXXXX
  • Bloqueie o uso de IFS e variáveis externas antes de parsear argumentos.

Exemplo de proteção contra PATH malicioso:

PATH="/bin:/usr/bin"
export PATH

Isso reduz risco de execução de binários inesperados durante o deploy.


14. Ferramentas de validação e testes

  • shellcheck: análise estática que detecta muitos anti-padrões e bugs comuns.
  • bats-core: framework de testes unitários para scripts Bash.
  • shfmt: formata código shell para consistência.
  • set -x, env DEBUG=1, e logs para debugging em CI.

Teste recomendado antes de merge/produção:

  • Teste por pares com shellcheck.
  • Execução em ambiente de staging replicando variáveis de ambiente.
  • Testes de tolerância a entradas malformadas.

15. Snippets e cheatsheet úteis

Cheat: modo estrito e trap básico

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
trap 'rc=$?; echo "Saindo com $rc" >&2; cleanup || true; exit $rc' EXIT

Loop robusto para ler linhas (preserva espaços e barras):

while IFS= read -r line; do
  printf '%s\n' "$line"
done < input.txt

Execução de comando com fallback e logging:

cmd_output=$(somecommand 2>&1) || { echo "somecommand falhou: $cmd_output" >&2; exit 1; }

Criar ficheiro temp e limpar automaticamente:

tmpfile=$(mktemp) || exit 1
trap 'rm -f "$tmpfile"' EXIT

Argument parsing simples com getopts:

while getopts ":f:o:" opt; do
  case $opt in
    f) file=$OPTARG ;;
    o) out=$OPTARG ;;
    \?) echo "Opção inválida: -$OPTARG" >&2; exit 1 ;;
  esac
done
shift $((OPTIND -1))

16. Alternativas e quando Bash NÃO é a melhor escolha

Bash é ótimo para gluing, manipulação de ficheiros e automações simples. No entanto, para tarefas complexas considere:

  • Python: melhor para lógica pesada, bibliotecas, manuseio de JSON e testes unitários.
  • Go/Rust: para binários com alto desempenho e distribuição independente.
  • Make/Ansible: para orquestração e repeatability em infra.

Se o script está crescendo, considere portar para uma linguagem com melhor ergonomia para testes e estruturas.


17. Maturidade e modelo de evolução de scripts

Pequeno roteiro para amadurecimento de um script desde protótipo até produção:

  1. Protótipo: funcional, mínimo, sem hardening.
  2. Revisão: adicionar shebang, citações, set -e, comentários.
  3. Harden: traps, validações, logs, permissões.
  4. Testes: adicionar testes unitários e integração.
  5. Produção: empacotar, adicionar CI, policy de deploy.

18. Critérios de aceitação

Um script pode ser considerado pronto para produção se:

  • Passa todos os testes automatizados.
  • Não gera warnings do shellcheck (nível mais alto quando aplicável).
  • Tem documentação básica (uso, flags, pré-requisitos).
  • Não executa comandos com privilégios elevados sem confirmação documentada.
  • Tem limpeza garantida de recursos temporários.

19. Exemplos de casos reais e contraexemplos quando falha

Caso: parsing de nomes com espaços

  • Falha comum: for f in $(ls *.txt); do …; done
  • Correção: for f in *.txt; do printf ‘%s\n’ “$f”; done

Caso: uso de set -e com comando dentro de if

  • Às vezes set -e pode não interromper o script quando o comando falha dentro de um teste condicional. Portanto, trate explicitamente verificações críticas.

Caso: harness de CI

  • Teste seus scripts em múltiplas imagens (alpine vs ubuntu) para capturar diferenças de sed, grep e utilitários coreutils. Em Alpine, BusyBox pode não suportar as mesmas opções; prefira POSIX ou verifique disponibilidade.

20. Compatibilidade e migração

  • Para máxima compatibilidade entre shells, mantenha-se dentro de POSIX sh quando possível.
  • Recursos do Bash moderno (arrays, process substitution <(), extglob) não funcionam em dash/sh.
  • Se precisar de compatibilidade, documente claramente a versão mínima requerida do Bash.

Resumo

  • Use shebangs conscientes (#/usr/bin/env bash para portabilidade; /bin/bash para determinismo).
  • Cite variáveis sempre: “$VAR”. Use arrays quando necessário.
  • Adote set -euo pipefail com traps para limpeza.
  • Prefira $(…) à crase, e opções longas para legibilidade.
  • Valide com shellcheck e escreva testes; migre para outra linguagem se o script crescer demais.

Resumo das principais ações rápidas:

  • Ative modo estrito e traps.
  • Use aspas e arrays.
  • Evite parsing de ls para listas de ficheiros.
  • Teste em imagens diferentes e use ferramentas de lint.

Obrigado por seguir estas práticas. Scripts mais previsíveis são mais seguros e mais fáceis de manter.

Autor
Edição

Materiais semelhantes

Instalar e usar Podman no Debian 11
Containers

Instalar e usar Podman no Debian 11

Apt‑pinning no Debian: guia prático
Administração de sistemas

Apt‑pinning no Debian: guia prático

Injete FSR 4 com OptiScaler em qualquer jogo
Tecnologia

Injete FSR 4 com OptiScaler em qualquer jogo

DansGuardian e Squid com NTLM no Debian Etch
Infraestrutura

DansGuardian e Squid com NTLM no Debian Etch

Corrigir erro de instalação no Android
Android

Corrigir erro de instalação no Android

KNetAttach: Pastas de Rede remota no KDE
KDE

KNetAttach: Pastas de Rede remota no KDE