Por que as quebras ‘~’ citadas e como corrigi-las


A maioria dos desenvolvedores, em algum momento, copiou e colou uma linha de exportação PATH em um arquivo de configuração do shell sem pensar duas vezes antes de citá-la. Esse único ato de memória muscular pode introduzir uma vulnerabilidade de segurança sutil e persistente.

A questão central: um literal ~ em PATH não aponta para onde a maioria dos usuários supõe que sim. Quando o caractere til aparece entre aspas duplas, o shell o trata como uma string simples em vez de expandi-lo para o diretório inicial do usuário. A entrada PATH resultante torna-se um caminho relativo, que é resolvido no diretório de trabalho atual, o que pode permitir que um binário plantado seja executado a partir de /tmpum repositório clonado ou um diretório de trabalho do CI. Quais programas realmente pesquisam essa entrada relativa varia: em meus testes no Linux, o Bash 5.1 expandiu o til nas próprias entradas PATH, enquanto o traço, envPython e Node encontraram um binário em ./~/.local/bin. Este artigo detalha como ocorrem falhas de segurança no til no PATH, fornece uma auditoria de um comando e fornece uma correção permanente. A mecânica subjacente foi demonstrada na pesquisa de shutdown3d.pl, que mostrou um binário sendo encontrado e executado a partir de um literal ./~/.local/bin diretório. Este artigo aborda bash e zsh; fish shell usa um mecanismo PATH diferente e não está sujeito a este problema específico.

Índice

Como a expansão Til realmente funciona no Bash e Zsh

As regras de expansão: quando ~ Torna-se /home/username

A expansão do til é uma fase discreta no pipeline de processamento do shell. O shell expande os tils antecipadamente, antes da expansão dos parâmetros, conforme POSIX §2.6, mas apenas sob condições sintáticas específicas. Para o ~ personagem para expandir para $HOMEele deve aparecer sem aspas e no início de uma palavra ou após dois pontos em um contexto de atribuição de variável. Essa última condição é crítica: os shells POSIX reconhecem valores separados por dois pontos em instruções de atribuição (como PATH=) e execute a expansão do til após cada delimitador de dois pontos. Isto é o que permite export PATH=$PATH:~/.local/bin para funcionar corretamente. A concha vê o ~ após dois pontos em uma atribuição, reconhece-a como elegível para expansão e substitui o caminho do diretório inicial do usuário.

A armadilha das citações: por que aspas duplas suprimem a expansão do til

Aspas duplas em scripts de shell preservam o valor literal da maioria dos caracteres incluídos nelas. As exceções são bem conhecidas: $ aciona expansão de variável, crases acionam substituição de comando e \ mantém seu significado de escape antes de certos caracteres. O ~ personagem não está entre essas exceções. Dentro de aspas duplas, ~ permanece um til literal. O POSIX define esse comportamento deliberadamente, mas continua sendo uma arma que atinge até mesmo desenvolvedores experientes.

A diferença entre uma exportação PATH correta e uma quebrada é um único par de aspas:


export PATH=$PATH:~/.local/bin
echo "$PATH"



export PATH="$PATH:~/.local/bin"
echo "$PATH"

A primeira forma permite que o shell se expanda ~ porque aparece sem aspas após dois pontos em uma tarefa. A segunda forma envolve tudo entre aspas duplas, o que expande $PATH (porque $ é uma exceção citada), mas deixa ~ intocado. O PATH resultante agora contém uma string literal ~/.local/bin em vez de um caminho absoluto.

As implicações de segurança de um literal ~ no CAMINHO

Resolução de caminho relativo: onde está ~/.local/bin Na verdade ponto?

Quando o shell encontra ~/.local/bin como uma string literal em PATH, ele não a reconhece como uma referência de diretório inicial. Em vez disso, ele o trata como um caminho relativo simples e acrescenta diretamente o diretório de trabalho atual; em /tmpo caminho de pesquisa resolvido se torna /tmp/~/.local/bin. Se um usuário executar cd /tmp e depois digita um comando, o programa pesquisa cada entrada PATH em ordem. Quando chega ao literal ~/.local/bin entrada, ele procura /tmp/~/.local/bin/.



set -euo pipefail

_demo_cleanup() {
  rm -rf /tmp/'~'
  echo "(demo) Cleanup complete. Malicious ls removed."
}
trap _demo_cleanup EXIT


export PATH="~/.local/bin:$PATH"


cd /tmp
mkdir -p '~/.local/bin'


cat > '/tmp/~/.local/bin/ls' << 'EOF'
#!/usr/bin/env bash
echo "You have been hijacked"
EOF
chmod +x '/tmp/~/.local/bin/ls'


echo "PATH order:"
echo "$PATH" | tr ':' '
' | grep -n '.'


echo "Running ls from /tmp via env (Bash itself tilde-expands PATH entries, env does not):"
env ls

Uma observação sobre a demonstração: ela chama env ls de propósito. Eu testei isso no Linux com Bash 5.1, e o Bash expande uma liderança ~ nas entradas PATH quando procura um comando, então um simples ls digitado no Bash não executa o arquivo plantado. Traço, enve programas que geram processos por meio de execvppesquisas de estilo (Python subprocess e do nó child_process nos meus testes) pesquise ./~/.local/bin. O comportamento pode diferir de acordo com o shell e a versão, então teste sua própria configuração.

Para que esta demonstração tenha sucesso, o literal ~ entrada deve preceder o diretório do sistema que contém o real ls (por exemplo, /bin ou /usr/bin) em PATH. Se você anexar em vez de preceder, o sistema ls será encontrado primeiro e o sequestro não ocorrerá. Verifique o pedido com echo "$PATH" | tr ':' ' ' | grep -n '.' antes de executar o teste.

O detalhe crítico: isso só funciona quando o diretório de trabalho atual do usuário contém um ~ diretório correspondente à estrutura PATH. Essa condição é comum em sistemas multiusuário: /tmp é gravável mundialmente e os invasores podem cometer ~diretórios nomeados para repositórios compartilhados.

Cenário de ataque: diretórios compartilhados, pastas temporárias e repositórios Git

Três vetores de ataque comuns exploram esse comportamento. Diretórios compartilhados como /tmp em sistemas multiusuário são alvos clássicos: qualquer usuário com acesso de gravação /tmp pode criar um ~/.local/bin árvore de diretórios e plante executáveis ​​lá; o sticky bit permite a criação, mas não impede esse ataque. Isso exige que o invasor tenha acesso de gravação a um diretório que a vítima usará como CWD, uma condição comumente satisfeita por diretórios graváveis ​​mundialmente, como /tmp ou por commit de acesso a um repositório compartilhado. Um invasor também pode explorar repositórios Git clonados, comprometendo um diretório chamado literalmente ~ (com .local/bin aninhado dentro). Quando uma vítima clona o repositório e executa comandos de dentro dele, os binários plantados tornam-se acessíveis através da entrada PATH corrompida. Os executores de CI/CD com diretórios de trabalho previsíveis enfrentam exposição semelhante, já que as etapas de construção geralmente são executadas em caminhos conhecidos onde um invasor com acesso de gravação de repositório coloca binários maliciosos.

A ordem de pesquisa PATH é o fator decisivo. O invasor só precisa do literal ~ entrada apareça antes da entrada legítima do sistema para o comando de destino. Se a entrada corrompida vier primeiro, o binário plantado vence.

Por que isso é fácil de perder

Os arquivos de inicialização do Shell acumulam alterações de PATH ao longo dos anos, muitas vezes copiadas de postagens de blog, trechos de documentação ou recomendações de colegas. Os desenvolvedores raramente auditam essas linhas depois de colá-las. O sistema funciona corretamente quase o tempo todo porque os usuários raramente têm um diretório chamado literalmente ~ em seu diretório de trabalho atual. Esta vulnerabilidade permanece inativa até que um diretório plantado apareça no local certo.

ShellCheck pode capturar parte disso. A regra SC2088 (“Til não expande entre aspas. Use $HOME.”) é acionada quando uma palavra entre aspas começa com um til, como export PATH="~/.local/bin:$PATH". No meu teste com ShellCheck 0.11, ele não sinalizou a forma mais comum export PATH="$PATH:~/.local/bin"portanto, uma execução limpa do ShellCheck não prova que seu PATH é seguro. Execute a auditoria abaixo também.

Tabela de referência de contexto de expansão

SintaxeTil se expande?Entrada PATH resultanteSeguro?
export PATH=$PATH:~/.local/binSim/home/user/.local/binSim
export PATH="$PATH:~/.local/bin"Não~/.local/bin (literal)Não
export PATH="$PATH:$HOME/.local/bin"Sim/home/user/.local/binSim
export PATH="${PATH}:~/.local/bin"Não~/.local/bin (literal)Não
path+=("~/.local/bin") (zsh)Não~/.local/bin (literal)Não
path+=(~/.local/bin) (zsh)Sim/home/user/.local/binSim

O padrão é consistente: $HOME é a escolha mais segura porque se expande entre aspas duplas. É imune a citar erros de contexto. Quer a atribuição esteja entre aspas, sem aspas, entre colchetes ou parte de uma operação de array zsh, $HOME sempre resolve para o caminho absoluto do diretório inicial. (Isso pressupõe $HOME é definido como um caminho absoluto, o que é verdadeiro em ambientes de login padrão, mas deve ser verificado em contextos de contêiner ou shell restrito.)

Como auditar seu PATH agora mesmo

O Comando de Auditoria One-Liner

Um único pipeline revela se alguma entrada PATH começa com um til literal:

echo "$PATH" | tr ':' '
' | grep '^~'

Nenhuma saída significa que o PATH está limpo. Qualquer linha começando com ~ indica uma entrada de problema. Por exemplo:

$ echo "$PATH" | tr ':' '
' | grep '^~'
~/.local/bin

Essa saída confirma um literal ~/.local/bin está no PATH, resolvendo em relação ao diretório de trabalho atual, em vez de apontar para o diretório inicial. Observe que isso reflete o PATH da sessão shell atual; para auditar o que será carregado em uma nova sessão, verifique também os dotfiles diretamente.

Pesquisando seus arquivos de configuração do Shell

Para localizar a linha de exportação incorreta em dotfiles:



grep -nE '^\s*(^#).*\b(A-Z)*PATH(A-Z)*\s*(+)?=\s*"(^")*~' \
     ~/.bashrc ~/.bash_profile ~/.zshrc ~/.profile 2>/dev/null

Este padrão captura qualquer variável que termine em PATH (incluindo MANPATH, GOPATHetc.) que contém um literal ~ entre aspas duplas, ignorando as linhas comentadas. Linhas sem aspas corretamente, como export PATH=$PATH:~/.local/bin não corresponderá, pois o til se expande corretamente dessa forma.

Exemplo de saída:

/home/user/.bashrc:14:export PATH="$PATH:~/.local/bin"

Isso informa ao desenvolvedor exatamente qual arquivo e número de linha contém a atribuição mal configurada, tornando a correção cirúrgica.

Como corrigir tils literais em PATH

O padrão recomendado: uso $HOME Em vez de ~

Porque $HOME é uma variável, ela se expande entre aspas duplas. Isso o torna seguro, independentemente do contexto de citação. A correção é uma substituição direta:


export PATH="$PATH:~/.local/bin"


export PATH="$PATH:$HOME/.local/bin"

Snippet de arquivo RC corrigido

Um bloco completo que pode ser copiado e colado para bash e zsh:






export PATH="$PATH:$HOME/.local/bin:$HOME/bin"




Encomendar é importante e envolve uma compensação de segurança. Colocando diretórios de usuários antes $PATH significa que os binários instalados pelo usuário têm precedência sobre os binários do sistema. Isto é conveniente para o desenvolvimento, mas significa um compromisso ~/.local/bin pode ocultar comandos do sistema. Colocando diretórios de usuários depois $PATH permite que os binários do sistema vençam, reduzindo a superfície para ataques de substituição binária, mas evitando substituições do usuário. A escolha certa depende do modelo de ameaça: em sistemas partilhados ou de produção, a ordenação do sistema é mais defensiva.

E quanto ao formulário não citado: é realmente seguro?

A forma não citada export PATH=$PATH:~/.local/bin funciona corretamente. Os shells POSIX executam a expansão do til no contexto de atribuição após os separadores de dois pontos, de modo que o ~ expande conforme o esperado. Porém, é frágil: um espaço inesperado no valor interrompe a atribuição. Usando $HOME entre aspas duplas é a solução mais limpa e funciona em todos os contextos de cotação.

Aplicando a correção e verificando

A justificativa para esta etapa de verificação: editar um arquivo rc altera shells futuros, mas sua sessão atual ainda carrega o antigo PATH. A obtenção do arquivo o recarrega e a afirmação abaixo confirma que nenhum til literal sobreviveu.


source ~/.bashrc || { echo "ERROR: ~/.bashrc failed to source. Check for syntax errors." >&2; return 1; }


_tilde_entries="$(echo "$PATH" | tr ':' '
' | grep '^~')"
if ( -n "$_tilde_entries" ); then
  echo "FAIL: Literal tilde still present in PATH:" >&2
  echo "$_tilde_entries" >&2
else
  echo "OK: No literal tilde entries found in PATH."
fi
unset _tilde_entries

Se a auditoria relatar “OK: nenhuma entrada literal de til encontrada em PATH”, a correção será confirmada. Se a sua modificação PATH estiver em ~/.bash_profile ou ~/.profile em vez de ~/.bashrcforneça esse arquivo ou abra um novo shell de login para confirmar se a alteração entrou em vigor.

Práticas defensivas para higiene PATH

Use ShellCheck em seu fluxo de trabalho do Dotfiles

Correndo shellcheck ~/.bashrc captura periodicamente muitos problemas de cotação, e um pipeline de CI executando ShellCheck em um repositório dotfiles os captura automaticamente. A regra SC2088 cobre palavras citadas que começam com um til, mas como observado acima ela falta "$PATH:~/.local/bin"portanto, mantenha a auditoria PATH junto com ela.

Evite anexar ao PATH várias vezes

O fornecimento repetido de arquivos de configuração do shell pode sobrecarregar o PATH com entradas duplicadas, dificultando a auditoria e retardando a pesquisa de comandos. Um ajudante idempotente evita isso:

pathadd() {
  
  
  case "$1" in
    /*) ;;
    *)
      printf 'pathadd: error: argument must be an absolute path, got: %s
' "$1" >&2
      return 1
      ;;
  esac
  case ":$PATH:" in
    *:"$1":*) ;;
    *) export PATH="${PATH:+$PATH:}$1" ;;
  esac
}

pathadd_front() {
  case "$1" in
    /*) ;;
    *)
      printf 'pathadd_front: error: argument must be an absolute path, got: %s
' "$1" >&2
      return 1
      ;;
  esac
  case ":$PATH:" in
    *:"$1":*) ;;
    *) export PATH="$1${PATH:+:$PATH}" ;;
  esac
}


pathadd "$HOME/.local/bin"
pathadd_front "$HOME/bin"

O pathadd a função anexa (os diretórios do sistema têm precedência), enquanto pathadd_front precede (os diretórios do usuário têm precedência). Ambos rejeitam qualquer argumento que não seja um caminho absoluto, evitando que os chamadores passem acidentalmente um literal ~ e reintroduzindo a vulnerabilidade. Eles também verificam se o diretório já existe em PATH antes de adicioná-lo, mantendo a variável limpa e inspecionável.

Auditoria de scripts de perfil compartilhado e de todo o sistema

Dotfiles individuais são apenas parte da imagem. Arquivos rc em nível de sistema também podem introduzir tils literais. Observe que /etc/environment (usado pelo PAM em sistemas baseados em Debian/Ubuntu) não é analisado por shell; ele não suporta expansão de til, então qualquer ~ há sempre um problema literal e sempre um problema. Audite os arquivos analisados ​​pelo shell e analisados ​​pelo PAM separadamente:


grep -rnE '^\s*(^#).*\b(A-Z)*PATH(A-Z)*\s*(+)?=\s*"(^")*~' \
     /etc/profile /etc/profile.d/ /etc/bash.bashrc 2>/dev/null


echo "--- /etc/environment (any ~ is always literal here) ---"
grep -n '~' /etc/environment 2>/dev/null

Em sistemas multiusuários, onde o risco relativo de exploração do caminho é mais alto, esses scripts de todo o sistema merecem o mesmo tratamento de auditoria.

Principais conclusões

Um citado ~ é um caractere literal, não uma referência ao diretório inicial. Um literal ~ em PATH cria uma entrada de diretório relativa que é resolvida no diretório de trabalho atual, produzindo uma condição explorável em diretórios compartilhados, repositórios clonados e ambientes de CI. Execute isso agora: echo "$PATH" | tr ':' ' ' | grep '^~'. Substitua cada ~ com $HOME dentro das atribuições PATH citadas. $HOME funciona em todos os contextos de cotação e elimina toda uma classe de configuração incorreta silenciosa. Faça disso um hábito.



Source link