Preâmbulo
Primeiro, eu diria que não é o caminho certo para resolver o problema. É um pouco como dizer " você não deve matar pessoas porque senão irá para a cadeia ".
Da mesma forma, você não cita sua variável porque, de outra forma, está apresentando vulnerabilidades de segurança. Você cita suas variáveis porque é errado não fazê-lo (mas se o medo da prisão pode ajudar, por que não)?
Um pequeno resumo para aqueles que acabaram de pular no trem.
Na maioria dos shells, deixar uma expansão variável sem aspas (embora isso (e o restante desta resposta) também se aplique à substituição de comando ( `...`
ou $(...)
) e expansão aritmética ( $((...))
ou $[...]
)) tem um significado muito especial. A melhor maneira de descrevê-lo é como invocar algum tipo de operador split + glob implícito¹ .
cmd $var
em outro idioma seria escrito algo como:
cmd(glob(split($var)))
$var
é primeiro dividido em uma lista de palavras de acordo com regras complexas que envolvem o $IFS
parâmetro especial (a parte dividida ) e, em seguida, cada palavra resultante dessa divisão é considerada um padrão que é expandido para uma lista de arquivos correspondentes (a parte glob ) .
Por exemplo, se $var
contiver *.txt,/var/*.xml
e $IFS
contiver ,
, cmd
seria chamado com vários argumentos, sendo o primeiro cmd
e os próximos os txt
arquivos no diretório atual e os xml
arquivos em /var
.
Se você quisesse chamar cmd
apenas com os dois argumentos literais cmd
e *.txt,/var/*.xml
, escreveria:
cmd "$var"
que seria em seu outro idioma mais familiar:
cmd($var)
O que queremos dizer com vulnerabilidade em um shell ?
Afinal, sabe-se desde o início que os scripts de shell não devem ser usados em contextos sensíveis à segurança. Certamente, OK, deixar uma variável sem citação é um bug, mas isso não pode causar tanto dano, pode?
Bem, apesar de alguém dizer a você que scripts de shell nunca devem ser usados para CGIs da Web, ou que, felizmente, a maioria dos sistemas não permite scripts de shell setuid / setgid hoje em dia, uma coisa que faz o shellshock (o bug bash remotamente explorável que fez o manchetes em setembro de 2014) revelado é que os shells ainda são amplamente utilizados onde provavelmente não deveriam: nos CGIs, nos scripts de gancho do cliente DHCP, nos comandos sudoers, invocados por (se não como ) comandos setuid ...
Às vezes sem saber. Por exemplo, system('cmd $PATH_INFO')
em um script php
/ perl
/ python
CGI chama um shell para interpretar essa linha de comando (para não mencionar o fato de que cmd
ele próprio pode ser um script shell e seu autor nunca esperou que fosse chamado de um CGI).
Você tem uma vulnerabilidade quando há um caminho para a escalada de privilégios, ou seja, quando alguém (vamos chamá-lo de atacante ) é capaz de fazer algo a que não se destina.
Invariavelmente, isso significa que o invasor fornece dados, que são processados por um usuário / processo privilegiado que, inadvertidamente, faz algo que não deveria estar fazendo, na maioria dos casos por causa de um bug.
Basicamente, você tem um problema quando seu código de buggy processa dados sob o controle do invasor .
Agora, nem sempre é óbvio de onde vêm esses dados , e muitas vezes é difícil saber se seu código processará dados não confiáveis.
No que diz respeito às variáveis, no caso de um script CGI, é bastante óbvio, os dados são os parâmetros CGI GET / POST e coisas como parâmetros de cookies, caminho, host ...
Para um script setuid (executando como um usuário quando chamado por outro), são os argumentos ou variáveis de ambiente.
Outro vetor muito comum são os nomes de arquivos. Se você estiver obtendo uma lista de arquivos de um diretório, é possível que os arquivos tenham sido plantados lá pelo invasor .
Nesse sentido, mesmo no prompt de um shell interativo, você pode estar vulnerável (ao processar arquivos em /tmp
ou ~/tmp
por exemplo).
Até mesmo um ~/.bashrc
pode ser vulnerável (por exemplo, bash
o interpretará quando chamado ssh
para executar o ForcedCommand
mesmo em git
implantações de servidor com algumas variáveis sob o controle do cliente).
Agora, um script não pode ser chamado diretamente para processar dados não confiáveis, mas pode ser chamado por outro comando que o faça. Ou seu código incorreto pode ser copiado e colado em scripts (por você, três anos depois ou por um de seus colegas). Um local em que é particularmente crítico é a resposta nos sites de perguntas e respostas, pois você nunca saberá onde as cópias do seu código podem acabar.
Direto aos negócios; quão ruim é isso?
Deixar uma variável (ou substituição de comando) sem aspas é de longe a fonte número um de vulnerabilidades de segurança associadas ao código do shell. Em parte porque esses erros geralmente se traduzem em vulnerabilidades, mas também porque é muito comum ver variáveis não citadas.
Na verdade, ao procurar vulnerabilidades no código do shell, a primeira coisa a fazer é procurar variáveis não citadas. É fácil identificar, geralmente um bom candidato, geralmente fácil rastrear os dados controlados pelo invasor.
Há um número infinito de maneiras pelas quais uma variável não citada pode se transformar em uma vulnerabilidade. Vou apenas dar algumas tendências comuns aqui.
Divulgação de informação
A maioria das pessoas encontra bugs associados a variáveis não citadas por causa da parte dividida (por exemplo, é comum os arquivos terem espaços em seus nomes hoje em dia e o espaço ter o valor padrão do IFS). Muitas pessoas vão ignorar a
parte glob . A parte glob é pelo menos tão perigosa quanto a
parte dividida .
Observar as entradas externas não autorizadas significa que o invasor pode fazer você ler o conteúdo de qualquer diretório.
No:
echo You entered: $unsanitised_external_input
se $unsanitised_external_input
contiver /*
, significa que o invasor pode ver o conteúdo de /
. Nada demais. Torna-se mais interessante, porém, com o /home/*
qual fornece uma lista de nomes de usuário na máquina /tmp/*
, /home/*/.forward
para dicas de outras práticas perigosas, /etc/rc*/*
para serviços ativados ... Não há necessidade de nomeá-los individualmente. Um valor de /*
/*/* /*/*/*...
apenas listará todo o sistema de arquivos.
Vulnerabilidades de negação de serviço.
Levando o caso anterior um pouco longe demais e temos um DoS.
Na verdade, qualquer variável não citada no contexto de lista com entrada não autorizada é pelo menos uma vulnerabilidade de DoS.
Mesmo os scripts de shell especialistas geralmente esquecem de citar coisas como:
#! /bin/sh -
: ${QUERYSTRING=$1}
:
é o comando no-op. O que poderia dar errado?
Isso deve ser atribuído $1
a $QUERYSTRING
se $QUERYSTRING
não estava definido. Essa é uma maneira rápida de tornar também possível um script CGI a partir da linha de comando.
Isso $QUERYSTRING
ainda é expandido e, como não é citado, o operador split + glob é chamado.
Agora, existem alguns globs que são particularmente caros para expandir. A /*/*/*/*
um é ruim o suficiente, já que significa diretórios listagem até 4 níveis abaixo. Além da atividade de disco e CPU, isso significa armazenar dezenas de milhares de caminhos de arquivo (40k aqui em uma VM de servidor mínima, 10k dos quais diretórios).
Agora /*/*/*/*/../../../../*/*/*/*
significa 40k x 10k e
/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*
é suficiente para trazer até a máquina mais poderosa de joelhos.
Experimente você mesmo (embora esteja preparado para travar ou travar sua máquina):
a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'
Obviamente, se o código for:
echo $QUERYSTRING > /some/file
Então você pode encher o disco.
Basta fazer uma pesquisa no google em shell cgi ou bash cgi ou ksh cgi , e você encontrará algumas páginas que mostram como escrever CGIs em shell. Observe como metade daqueles que processam parâmetros são vulneráveis.
Até o próprio David Korn
é vulnerável (veja o manuseio de cookies).
até vulnerabilidades arbitrárias de execução de código
A execução arbitrária de código é o pior tipo de vulnerabilidade, pois se o invasor pode executar qualquer comando, não há limite para o que ele pode fazer.
Essa geralmente é a parte dividida que leva a eles. Essa divisão resulta em vários argumentos a serem passados para comandos quando apenas um é esperado. Enquanto o primeiro deles será usado no contexto esperado, os outros estarão em um contexto diferente, sendo potencialmente interpretados de maneira diferente. Melhor com um exemplo:
awk -v foo=$external_input '$2 == foo'
Aqui, a intenção era atribuir o conteúdo da
$external_input
variável shell à foo
awk
variável.
Agora:
$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux
A segunda palavra resultante da divisão de $external_input
não é atribuída, foo
mas considerada como awk
código (aqui que executa um comando arbitrário:) uname
.
Isso é um problema especialmente para os comandos que podem executar outros comandos ( awk
, env
, sed
(GNU um), perl
, find
...) especialmente com as variantes GNU (que aceitam opções depois de argumentos). Às vezes, você não suspeitaria de comandos capazes de executar outras pessoas como ksh
, bash
ou zsh
's [
ou printf
...
for file in *; do
[ -f $file ] || continue
something-that-would-be-dangerous-if-$file-were-a-directory
done
Se criarmos um diretório chamado x -o yes
, o teste se tornará positivo, porque estamos avaliando uma expressão condicional completamente diferente.
Pior, se criarmos um arquivo chamado x -a a[0$(uname>&2)] -gt 1
, com pelo menos todas as implementações do ksh (que inclui a sh
maioria dos Unices comerciais e alguns BSDs), que será executado uname
porque esses shells executam uma avaliação aritmética nos operadores de comparação numérica do [
comando.
$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux
O mesmo acontece com bash
um nome de arquivo como x -a -v a[0$(uname>&2)]
.
Obviamente, se eles não conseguirem uma execução arbitrária, o atacante pode se contentar com menos danos (o que pode ajudar a obter uma execução arbitrária). Qualquer comando que possa gravar arquivos ou alterar permissões, propriedade ou ter qualquer efeito principal ou colateral pode ser explorado.
Todo tipo de coisa pode ser feita com nomes de arquivos.
$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done
E você acaba ..
gravando (recursivamente com o GNU
chmod
).
Os scripts que executam o processamento automático de arquivos em áreas publicamente graváveis como /tmp
devem ser escritos com muito cuidado.
Sobre [ $# -gt 1 ]
Isso é algo que eu acho exasperante. Algumas pessoas se preocupam em saber se uma expansão específica pode ser problemática para decidir se podem omitir as aspas.
É como dizer. Ei, parece que $#
não pode estar sujeito ao operador split + glob, vamos pedir ao shell para dividir + glob . Ou seja, vamos escrever um código incorreto apenas porque é improvável que o bug seja atingido .
Agora, quão improvável é isso? OK, $#
(ou $!
, $?
ou qualquer substituição aritmética) pode conter apenas dígitos (ou -
para alguns) de modo que o glob parte está fora. Para a divisão de parte a fazer algo, porém, tudo o que precisamos é para $IFS
conter dígitos (ou -
).
Com algumas conchas, $IFS
pode ser herdado do ambiente, mas se o ambiente não for seguro, o jogo terminará assim mesmo.
Agora, se você escrever uma função como:
my_function() {
[ $# -eq 2 ] || return
...
}
O que isso significa é que o comportamento de sua função depende do contexto em que é chamada. Ou, em outras palavras, $IFS
torna-se uma das entradas para ele. Estritamente falando, quando você escreve a documentação da API para sua função, deve ser algo como:
# my_function
# inputs:
# $1: source directory
# $2: destination directory
# $IFS: used to split $#, expected not to contain digits...
E o código que chama sua função precisa garantir $IFS
que não contenha dígitos. Tudo isso porque você não sentiu vontade de digitar esses dois caracteres de aspas duplas.
Agora, para que esse [ $# -eq 2 ]
bug se torne uma vulnerabilidade, você precisaria, de alguma forma, do valor de $IFS
ficar sob controle do invasor . É concebível que isso normalmente não aconteça a menos que o invasor consiga explorar outro bug.
Isso não é inédito. Um caso comum é quando as pessoas esquecem de limpar os dados antes de usá-los na expressão aritmética. Já vimos acima que ele pode permitir a execução arbitrária de código em alguns shells, mas em todos eles, permite
ao invasor atribuir a qualquer variável um valor inteiro.
Por exemplo:
n=$(($1 + 1))
if [ $# -gt 2 ]; then
echo >&2 "Too many arguments"
exit 1
fi
E com um $1
valor with (IFS=-1234567890)
, essa avaliação aritmética tem o efeito colateral das configurações IFS e o próximo [
comando falha, o que significa que a verificação de muitos argumentos é ignorada.
E quando o operador split + glob não é chamado?
Há outro caso em que são necessárias aspas em torno de variáveis e outras expansões: quando é usado como padrão.
[[ $a = $b ]] # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac
não teste se $a
e $b
é o mesmo (exceto com zsh
), mas se $a
corresponde ao padrão em $b
. E você precisa citar $b
se deseja comparar como strings (a mesma coisa em "${a#$b}"
ou "${a%$b}"
ou "${a##*$b*}"
onde $b
deve ser citada, se não for para ser tomada como padrão).
O que isso significa é que [[ $a = $b ]]
pode retornar verdadeiro nos casos em que $a
é diferente de $b
(por exemplo, quando $a
é anything
e $b
é *
) ou pode retornar falso quando são idênticos (por exemplo, quando ambos $a
e $b
são [a]
).
Isso pode criar uma vulnerabilidade de segurança? Sim, como qualquer bug. Aqui, o invasor pode alterar o fluxo de código lógico do seu script e / ou quebrar as suposições que seu script está fazendo. Por exemplo, com um código como:
if [[ $1 = $2 ]]; then
echo >&2 '$1 and $2 cannot be the same or damage will incur'
exit 1
fi
O atacante pode ignorar a verificação passando '[a]' '[a]'
.
Agora, se nem a correspondência de padrões nem o operador split + glob se aplicam, qual é o perigo de deixar uma variável sem aspas?
Eu tenho que admitir que escrevo:
a=$b
case $a in...
Lá, citar não prejudica, mas não é estritamente necessário.
No entanto, um efeito colateral de omitir aspas nesses casos (por exemplo, nas respostas das perguntas e respostas) é que ele pode enviar uma mensagem errada aos iniciantes: que pode ser correto não citar variáveis .
Por exemplo, eles podem começar a pensar que, se a=$b
estiver tudo bem, export a=$b
seria bom (o que não ocorre em muitos shells, como nos argumentos para o export
comando e no contexto da lista) ou env a=$b
.
Que tal zsh
?
zsh
corrigiu a maioria desses constrangimentos. Em zsh
(pelo menos quando não estiver no modo de emulação sh / ksh), se você desejar dividir , globbing ou correspondência de padrões , precisará solicitá-lo explicitamente: $=var
dividir e $~var
glob ou para que o conteúdo da variável seja tratado como um padrão.
No entanto, a divisão (mas não o globbing) ainda é feita implicitamente na substituição de comandos sem aspas (como em echo $(cmd)
).
Além disso, um efeito colateral às vezes indesejável de não citar variáveis é a remoção de vazios . O zsh
comportamento é semelhante ao que você pode obter em outros shells, desativando completamente o globbing (com set -f
) e dividindo (com IFS=''
). Ainda em:
cmd $var
Não haverá divisão + glob , mas se $var
estiver vazio, em vez de receber um argumento vazio, cmd
não receberá nenhum argumento.
Isso pode causar erros (como o óbvio [ -n $var ]
). Isso pode quebrar as expectativas e suposições de um script e causar vulnerabilidades, mas não posso criar um exemplo não muito abrangente agora).
E quando você faz precisa da divisão + glob operador?
Sim, normalmente é quando você deseja deixar sua variável sem aspas. Mas, então, você precisa ajustar corretamente os operadores de divisão e glob antes de usá-lo. Se você deseja apenas a parte dividida e não a parte glob (que é o caso na maioria das vezes), é necessário desativar o globbing ( set -o noglob
/ set -f
) e corrigir $IFS
. Caso contrário, você também causará vulnerabilidades (como o exemplo de CGI de David Korn mencionado acima).
Conclusão
Em resumo, deixar uma variável (ou substituição de comando ou expansão aritmética) sem citação em shells pode ser muito perigoso, especialmente quando feito em contextos errados, e é muito difícil saber quais são esses contextos errados.
Essa é uma das razões pelas quais é considerada uma má prática .
Obrigado pela leitura até agora. Se isso passar por sua cabeça, não se preocupe. Não se pode esperar que todos compreendam todas as implicações de escrever seu código da maneira que o escrevem. É por isso que temos
recomendações de boas práticas , para que possam ser seguidas sem necessariamente entender o porquê.
(e, caso ainda não seja óbvio, evite escrever códigos confidenciais de segurança nos shells).
E cite suas variáveis nas suas respostas neste site!
¹Em ksh93
e pdksh
e derivados, a expansão de braçadeira também é executada, a menos que o globbing esteja desativado (no caso de ksh93
versões até ksh93u +, mesmo quando a braceexpand
opção está desativada).