Como posso garantir com segurança que uma variável contenha apenas um nome de arquivo válido?

8

Dado o script abaixo, como posso garantir que o argumento contenha apenas um nome de arquivo válido dentro /home/charlesingalls/e não um caminho ( ../home/carolineingalls/) ou curinga, etc?

Eu só quero que o script possa excluir um único arquivo do diretório codificado fornecido. Este script será executado como um usuário privilegiado.

#!/bin/bash

rm -f /home/charlesingalls/"$1"
Aaron Cicali
fonte
2
Se você não deseja suportar "foo / bar", verifique se ele não contém /. Os curingas não são interpretados entre aspas.
Random832
3
Se você estiver excluindo um arquivo , não use -rcom rm. rm -ré para exclusão recursiva de um diretório e de todos os arquivos e diretórios abaixo dele. Só é útil ao excluir diretórios. De maneira mais geral, não faça culto à carga. ou seja, não copie apenas as coisas que parecem úteis em sua linha de comando ou script sem entender o que elas fazem ou como funcionam. Os deuses do avião que trazem a carga mágica podem ficar com raiva e excluir todos os seus arquivos.
18716
Bom ponto em relação à bandeira -r - eu entendo seu uso, mas não estava pensando claramente.
Aaron Cicali 19/07/2016

Respostas:

7

Se você deseja excluir apenas um arquivo /home/charlesingalls(e não um arquivo em um subdiretório), é fácil: basta verificar se o argumento não contém a /.

case "$1" in
  */*) echo 1>&2 "Refusing to remove a file in another directory"; exit 2;;
  *) rm -f /home/charlesingalls/"$1";;
esac

Isso é executado rmmesmo que o argumento seja .ou ..ou vazio, mas nesse caso rmfalhará inofensivamente ao excluir um diretório.

Os curingas não são relevantes aqui, pois nenhuma expansão de curinga é realizada.

Isso é seguro mesmo na presença de links simbólicos: se o arquivo for um link simbólico, o link simbólico (que está dentro /home/charlesingalls) será removido e o destino desse link não será afetado.

Observe que isso pressupõe que /home/charlesingallsnão pode ser movido ou alterado. Isso deve ser bom se o diretório for codificado no script, mas se for determinado a partir de variáveis, a determinação poderá não ser mais válida no momento em que o rmcomando for executado.

Com base nas informações adicionais de que o argumento é um nome de host virtual, você deve fazer a lista de permissões em vez da lista negra: verifique se o nome é um nome de host virtual sensato, em vez de apenas proibir barras. Verifico se o nome começa com uma letra minúscula ou dígito e se não contém caracteres que não sejam letras minúsculas, dígitos, pontos e traços.

LC_CTYPE=C LC_COLLATE=C
case "$1" in
  *[!-.0-9a-z]*|[!0-9a-z]*) echo >&2 "Invalid host name"; exit 2;;
  *) rm -f /home/charlesingalls/"$1";;
esac
Gilles 'SO- parar de ser mau'
fonte
Nesse caso, os arquivos são arquivos de configuração do servidor da web que foram gerados por um processo anterior. Todos eles têm o mesmo nível de importância (cada um constitui um host virtual). No entanto, o diretório em que eles existem é adjacente a diretórios semelhantes e todos eles existem em um diretório que pertence ao servidor da web. Esse script específico destina-se a permitir a exclusão de configurações de host virtual somente em um determinado diretório.
Aaron Cicali 19/07/2016
essa abordagem faz sentido para você desde que esse caso de uso? Agradeço sua experiência e admitirei que há momentos em que "trouxe uma bazuca para um tiroteio" para automatizar algo no linux.
Aaron Cicali 19/07/19
@AaronCicali Por que o script pode excluir qualquer uma das configurações do host e não apenas uma que pertence à entidade que fez a solicitação original? Por que o nome do host virtual não foi validado primeiro (então não conteria caracteres especiais)?
Gilles 'SO- stop be evil'
A entidade que faz a solicitação original é uma GUI que realmente tem permissão para remover qualquer um dos hosts virtuais nessa pasta. O nome do host virtual é validado primeiro. Ele vem de uma lista de hosts virtuais criados anteriormente (nomes de domínio). Acredito que é o trabalho desse script garantir que ele seja o mais seguro possível, sem depender da segurança de outra parte do aplicativo. Ou seja, que ele só pode excluir arquivos deste diretório específico. Também será necessário fazer algum trabalho de limpeza adicional.
Aaron Cicali 19/07/2016
@AaronCicali Ok, como uma verificação de sanidade adicional, isso faz sentido. Nesse caso, você deve colocar na lista de permissões: aceite apenas nomes que pareçam nomes de host plausíveis. Se você não permitir que subdomínios, você pode até proibir.
Gilles 'SO parada sendo maus'
10

Esta resposta assume que $1é permitido incluir subdiretórios. Se você estiver interessado no caso mais simples, onde $1deve haver um nome de diretório simples, consulte uma das outras respostas.


Os curingas não são expandidos quando estiver entre aspas duplas. Como $1está entre aspas duplas, os curingas não são um problema.

Os ../links simbólicos e podem obscurecer a localização real de um arquivo. Abaixo, são mostrados testes para determinar se o arquivo está realmente, e não apenas aparentemente, no caminho que queremos.

Sistemas mais recentes: usando realpath

Quanto a descobrir se o arquivo é realmente se o arquivo está realmente abaixo /home/charlesingalls/ou não, você pode usar realpath:

realpath --relative-base=/home/charlesingalls/ "/home/charlesingalls/$1"  | grep -q '^/' && exit 1

As execuções acima exit 1se o arquivo especificado pelo $1é em qualquer lugar que não seja sob o diretório /home/charlesingalls/. realpathcanoniza todo o caminho, eliminando os links simbólicos e ../.

realpath faz parte do GNU coreutils e deve estar disponível em qualquer sistema Linux.

realpathrequer GNU coreutils 8.15 (janeiro de 2012) ou melhor .

Exemplos

Para demonstrar como o realpath segue ../para determinar a localização real de um arquivo (por exemplo, a -qopção grep é omitida para que a saída real do grep seja visível):

$ touch /tmp/test
$ realpath --relative-base=$HOME "$HOME/../../tmp/test" | grep '^/' && echo FAIL
/tmp/test
FAIL

Para demonstrar como segue os links simbólicos:

$ ln -s /tmp/test ~/test
$ realpath --relative-base=$HOME "$HOME/test" | grep '^/' && echo FAIL
/tmp/test
FAIL

Sistemas mais antigos: usando readlink -e

readlinktambém é capaz de cononicalizar um caminho, seguindo os links simbólicos e ../:

readlink -e "$HOME/test" | grep -q "^$HOME" || exit 1

Usando os mesmos arquivos de exemplo:

$ readlink -e "$HOME/../../tmp/test" | grep "$HOME" || echo FAIL
FAIL
$ readlink -e "$HOME/test" | grep "^$HOME" || echo FAIL
FAIL

Além de estar disponível em sistemas GNU mais antigos, versões do readlinkestão disponíveis no BSD.

John1024
fonte
Meu Ubuntu 14.04 de coreutilsnão terrealpath
heemayl
1
Parece ser mais do que estou procurando, mas infelizmente meu servidor Centos 6.5 não tem caminho real. Não estou em posição de instalá-lo. Pesquisando no Google aparece menções de "readlink -f" substituindo caminho real, mas ainda não o fiz funcionar.
Aaron Cicali 19/07/2016
1
A legenda menciona -f("todos, exceto o último componente deve existir") e os exemplos usam -e("todos os componentes devem existir"), o que é um pouco confuso.
Isanae
2
@AaronCicali Esta definitivamente não é a resposta que você precisa, porque é perigosamente errada. Você não deve resolver links simbólicos . O alvo do link simbólico pode mudar entre o horário em que você o verifica e o horário em que o usa. (É uma categoria bem conhecida de erros de design ). Além disso, não faz sentido resolver links simbólicos, uma vez que rmage sobre o argumento em si, não sobre seu alvo.
Gilles 'SO- stop be evil'
1
Dica: use grep -qpara evitar a grepsaída de linhas correspondentes. Você ainda obtém o status de saída &&e ||ainda trabalha exatamente como está acostumado.
um CVn 21/07
2

Se você deseja proibir completamente os caminhos, a maneira mais simples é testar se a variável contém uma barra ( /). Na festança:

if [[ "$1" = */* ]] ; then...

Isso bloqueará todos os caminhos, inclusive foo/bar. Você poderia testar .., mas isso deixaria a possibilidade de links simbólicos apontarem para diretórios fora do caminho de destino.

Se você deseja permitir apenas a exclusão de um único arquivo, acho que não deveria estar usando rm -r.


Além disso, dependendo do que você está fazendo, você pode usar as permissões de arquivo do sistema para permitir apenas a exclusão de arquivos que o usuário pode excluir por conta própria. Algo assim:

su charlesingalls -c "rm /home/charlesingalls/'$1'"

Embora, como o @Gilles comentou, isso tenha um problema de citação: falhará se $1contiver uma única citação; portanto, a variável deve ser testada para isso primeiro (com, por exemplo, if [[ "$1" = *\'* ]] ; then fail...ou melhor, colocando na lista branca um conjunto sensível de caracteres) ou o nome do arquivo passado uma variável de ambiente com por exemplo

file="$1" su charlesingalls -c 'rm "/home/charlesingalls/$file"'
ilkkachu
fonte
Bom ponto em relação à bandeira -r, que realmente não deveria estar lá. Removendo agora ...
Aaron Cicali
Observe que seu sucomando está quebrado porque a citação está errada. Você está executando um comando com o argumento interpolado como um snippet de shell. Por exemplo, se o argumento for $(touch foo), seu código será executado touch foo.
Gilles 'SO- stop be evil' ''
@ Gilles, porcaria, eu sabia que tinha que haver algo errado. citar aninhado não é meu amigo favorito. Acho que a versão atual funciona melhor, aspas duplas avaliarão a variável no shell externo, e aspas simples impedirão que ela seja avaliada no shell interno. Sem garantia.
ilkkachu
@ilkkachu Essa versão falha se o argumento contiver uma única citação. (Mas apenas nesse caso, o que facilita muito a validação do que quando se usa aspas duplas.) É impossível interpolar diretamente uma sequência arbitrária. Você precisa massagear a string (por exemplo, substituir all 'by '\'') ou passá-la por outro canal, como uma variável de ambiente (que é o que eu faria aqui file_to_remove="$1" su -c 'rm "/home/charlesingalls/$file_to_remove"':). Acontece que o nome do arquivo deve ser um nome de host virtual, portanto, rejeitar todos os caracteres especiais também seria aceitável aqui.
Gilles 'SO- stop be evil'