Se você acompanha o unix.stackexchange.com há algum tempo, esperamos que você já saiba que deixar uma variável sem aspas no contexto da lista (como em echo $var
) nos shells Bourne / POSIX (zsh é a exceção) tem um significado muito especial e não deve ser feito, a menos que você tenha uma boa razão para fazê-lo.
É discutido longamente em uma série de perguntas e respostas aqui (Exemplos: ? Por que o meu shell choke script em espaços ou outros caracteres especiais , quando é colocar aspas necessário? , Expansão de uma variável shell e efeito de glob e dividido sobre ele , Citado vs expansão de cadeia não citada)
Esse tem sido o caso desde o lançamento inicial do shell Bourne no final dos anos 70 e não foi alterado pelo shell Korn (um dos maiores arrependimentos de David Korn (pergunta nº 7) ) ou bash
que copiou principalmente o shell Korn, e é isso como isso foi especificado pelo POSIX / Unix.
Agora, ainda estamos vendo várias respostas aqui e até ocasionalmente código de shell publicamente divulgado, onde as variáveis não são citadas. Você pensaria que as pessoas já teriam aprendido até agora.
Na minha experiência, existem principalmente três tipos de pessoas que omitem citar suas variáveis:
iniciantes. Essas podem ser desculpadas, pois é uma sintaxe completamente não intuitiva. E é nosso papel neste site educá-los.
pessoas esquecidas.
pessoas que não estão convencidas, mesmo depois de repetidas marteladas, que pensam que certamente o autor do shell Bourne não pretendia citar todas as nossas variáveis .
Talvez possamos convencê-los se expormos o risco associado a esse tipo de comportamento.
Qual é a pior coisa que pode acontecer se você esquecer de citar suas variáveis. É realmente tão ruim assim?
De que tipo de vulnerabilidade estamos falando aqui?
Em que contextos isso pode ser um problema?
Respostas:
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¹ .em outro idioma seria escrito algo como:
$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 primeirocmd
e os próximos ostxt
arquivos no diretório atual e osxml
arquivos em/var
.Se você quisesse chamar
cmd
apenas com os dois argumentos literaiscmd
e*.txt,/var/*.xml
, escreveria:que seria em seu outro idioma mais familiar:
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 scriptphp
/perl
/python
CGI chama um shell para interpretar essa linha de comando (para não mencionar o fato de quecmd
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 chamadossh
para executar oForcedCommand
mesmo emgit
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:
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:
:
é 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):
Obviamente, se o código for:
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:
Aqui, a intenção era atribuir o conteúdo da
$external_input
variável shell àfoo
awk
variável.Agora:
A segunda palavra resultante da divisão de
$external_input
não é atribuída,foo
mas considerada comoawk
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 comoksh
,bash
ouzsh
's[
ouprintf
...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 ash
maioria dos Unices comerciais e alguns BSDs), que será executadouname
porque esses shells executam uma avaliação aritmética nos operadores de comparação numérica do[
comando.O mesmo acontece com
bash
um nome de arquivo comox -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.
E você acaba
..
gravando (recursivamente com o GNUchmod
).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:
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: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:
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.
não teste se
$a
e$b
é o mesmo (exceto comzsh
), 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:
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:
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,seria bom (o que não ocorre em muitos shells, como nos argumentos para oexport a=$b
export
comando e no contexto da lista) ou.env a=$b
Que tal
zsh
?zsh
corrigiu a maioria desses constrangimentos. Emzsh
(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 (comset -f
) e dividindo (comIFS=''
). Ainda em: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
epdksh
e derivados, a expansão de braçadeira também é executada, a menos que o globbing esteja desativado (no caso deksh93
versões até ksh93u +, mesmo quando abraceexpand
opção está desativada).fonte
[[
, apenas o RHS das comparações precisa ser citado:if [[ $1 = "$2" ]]; then
[[ $* = "$var" ]]
não é o mesmo que[[ "$*" = "$var" ]]
se o primeiro caractere de$IFS
não seja espaçobash
(e tambémmksh
se$IFS
estiver vazio, nesse caso, não tenho certeza do que$*
é igual, devo gerar um bug?)).foo='bar; rm *'
, não, não será, no entanto, listará o conteúdo do diretório atual que pode ser considerado uma divulgação de informações.print $foo
inksh93
(ondeprint
está o substituto paraecho
esse endereço, algumas de suas deficiências) possui uma vulnerabilidade de injeção de código (por exemplo, comfoo='-f%.0d z[0$(uname>&2)]'
) (você realmente precisaprint -r -- "$foo"
.echo "$foo"
ainda está errado e não pode ser corrigido (embora geralmente seja menos prejudicial)).[Inspirado nesta resposta por cas .]
Mas e se …?
Não sei dizer ; isso falhará se for a string vazia.
grep "$ignorecase" other_grep_args
$ignorecase
Resposta:
Conforme discutido na outra resposta, isso ainda falhará se
IFS
contiver um-
ou umi
. Se você garantiu queIFS
não contém nenhum caractere em sua variável (e tem certeza de que sua variável não contém caracteres glob), isso provavelmente é seguro.Mas há uma maneira mais segura (embora seja um pouco feia e pouco intuitiva): use
${ignorecase:+"$ignorecase"}
. Na especificação POSIX Shell Command Language , em 2.6.2 Expansion Parameter ,O truque aqui, tal como ela é, é que estamos usando
ignorecase
como oparameter
e"$ignorecase"
como oword
. Então${ignorecase:+"$ignorecase"}
significaIsso nos leva aonde queremos ir: se a variável estiver definida como uma string vazia, ela será "removida" (toda essa expressão complicada não será avaliada em nada - nem mesmo uma string vazia), e se a variável tiver uma variável não valor vazio, obtemos esse valor, citado.
Mas e se …?
Resposta:
Você pode pensar que este é um caso de usoNão! Resista à tentação de pensar em usareval
.eval
aqui.Novamente, se você garantiu que
IFS
não contém nenhum caractere em sua variável (exceto os espaços que você deseja honrar) e tem certeza de que sua variável não contém caracteres glob, provavelmente o acima é seguro.Mas, se você estiver usando o bash (ou ksh, zsh ou yash), existe uma maneira mais segura: use uma matriz:
Do bash (1) ,
Portanto,
"${criteria[@]}"
expande para (no exemplo acima) o zero, dois ou quatro elementos dacriteria
matriz, cada um deles citado. Em particular, se nenhuma das condições s for verdadeira, acriteria
matriz não terá conteúdo (conforme definido pelacriteria=()
instrução) e"${criteria[@]}"
avalia como nada (nem mesmo uma cadeia vazia inconveniente).Isso se torna especialmente interessante e complicado quando você lida com várias palavras, algumas das quais são dinâmicas (usuário), que você não conhece antecipadamente e podem conter espaço (s) ou outro (s) caractere (s) especial (is). Considerar:
Observe que
$fname
é citado toda vez que é usado. Isso funciona mesmo se o usuário digitar algo comofoo bar
oufoo*
;"${criteria[@]}"
avalia como-name "foo bar"
ou-name "foo*"
. (Lembre-se de que cada elemento da matriz é citado.)Matrizes não funcionam em todos os shells POSIX; matrizes são um ksh / bash / zsh / yash-ism. Exceto ... há uma matriz que todos os shells suportam: a lista de argumentos, também conhecida como
"$@"
. Se você concluiu a lista de argumentos com a qual foi chamado (por exemplo, copiou todos os "parâmetros posicionais" (argumentos) em variáveis ou os processou), poderá usar a lista arg como uma matriz:A
"$@"
construção (que, historicamente, veio primeiro) tem a mesma semântica que - expande cada argumento (isto é, cada elemento da lista de argumentos) para uma palavra separada, como se você tivesse digitado ."${name[@]}"
"$1" "$2" "$3" …
Extraindo da especificação POSIX Shell Command Language , em 2.5.2 Parâmetros Especiais ,
O texto completo é um tanto enigmático; o ponto principal é que ele especifica que
"$@"
deve gerar zero campos quando não houver parâmetros posicionais. Nota histórica: quando"$@"
foi introduzido pela primeira vez no shell Bourne (predecessor do bash) em 1979, havia um bug que"$@"
era substituído por uma única string vazia quando não havia parâmetros posicionais; consulte O que${1+"$@"}
significa em um script de shell e como ele difere"$@"
? , A família tradicional Bourne Shell , O que${1+"$@"}
significa ... e onde é necessário? e"$@"
versus${1+"$@"}
.As matrizes também ajudam na primeira situação:
____________________
PS (csh)
Isso deve ser óbvio, mas, para o benefício de pessoas que são novas aqui: csh, tcsh, etc., não são shells Bourne / POSIX. Eles são uma família totalmente diferente. Um cavalo de uma cor diferente. Um outro jogo de bola. Uma raça diferente de gato. Aves de outra pena. E, mais particularmente, uma lata de vermes diferente.
Parte do que foi dito nesta página se aplica ao csh; como: é uma boa ideia citar todas as suas variáveis, a menos que você tenha um bom motivo para não fazê-lo e tenha certeza de que sabe o que está fazendo. Mas, no csh, toda variável é uma matriz - acontece que quase toda variável é uma matriz de apenas um elemento e age de maneira semelhante a uma variável comum do shell nos shells Bourne / POSIX. E a sintaxe é muito diferente (e eu quero dizer muito ). Portanto, não falaremos mais sobre os reservatórios da família csh aqui.
fonte
csh
você prefere usar$var:q
do"$var"
que o último não funciona para variáveis que contêm caracteres de nova linha (ou se você deseja citar os elementos da matriz individualmente, em vez de juntá-los a espaços em um único argumento).bitrate="--bitrate 320"
trabalha com${bitrate:+$bitrate}
ebitrate=--bitrate 128
não funciona${bitrate:+"$bitrate"}
porque quebra o comando. É seguro usar${variable:+$variable}
com não"
?$@
para outra coisa), ou simplesmente se recusa a usar um array por outros motivos, refiro-me você "Como discutido na outra resposta". ... (continua)${variable:+$variable}
sem"
) falhará se IFS contém-
,0
,2
,3
,a
,b
,e
,i
,r
out
.${bitrate:+$bitrate}
Eu era cético em relação à resposta de Stéphane, no entanto, é possível abusar
$#
:ou $ ?:
Estes são exemplos inventados, mas o potencial existe.
fonte