Implikasi keamanan dari lupa mengutip variabel dalam bash / POSIX shells

206

Jika Anda telah mengikuti unix.stackexchange.com untuk sementara waktu, semoga Anda tahu sekarang bahwa meninggalkan variabel yang tidak dikutip dalam konteks daftar (seperti pada echo $var) di shell Bourne / POSIX (zsh menjadi pengecualian) memiliki arti yang sangat istimewa dan tidak boleh dilakukan kecuali Anda memiliki alasan yang sangat bagus untuk itu.

Ini dibahas panjang lebar di sejumlah Q & A disini (Contoh: ? Mengapa saya shell script yang tersedak spasi atau karakter khusus lainnya , Kapan double-mengutip diperlukan? , Perluasan variabel shell dan efek gumpal dan perpecahan di atasnya , Dikutip vs ekspansi string yang tidak dikutip)

Itulah yang terjadi sejak rilis pertama dari shell Bourne pada akhir 70-an dan belum diubah oleh shell Korn (salah satu penyesalan terbesar David Korn (pertanyaan # 7) ) atau bashyang sebagian besar menyalin shell Korn, dan itulah bagaimana yang telah ditentukan oleh POSIX / Unix.

Sekarang, kami masih melihat sejumlah jawaban di sini dan bahkan kadang-kadang dirilis kode shell di mana variabel tidak dikutip. Anda akan berpikir orang akan belajar sekarang.

Dalam pengalaman saya, terutama ada 3 jenis orang yang tidak mengutip variabel mereka:

  • pemula. Mereka dapat dimaafkan karena memang itu sintaks yang benar-benar tidak intuitif. Dan itu adalah peran kami di situs ini untuk mendidik mereka.

  • orang yang pelupa.

  • orang-orang yang tidak yakin bahkan setelah berulang kali memalu, yang berpikir bahwa pastinya penulis shell Bourne tidak bermaksud kita untuk mengutip semua variabel kita .

Mungkin kita bisa meyakinkan mereka jika kita mengekspos risiko yang terkait dengan perilaku semacam ini.

Apa hal terburuk yang mungkin terjadi jika Anda lupa mengutip variabel Anda. Apakah itu benar-benar yang buruk?

Kerentanan macam apa yang kita bicarakan di sini?

Dalam konteks apa itu bisa menjadi masalah?

Stéphane Chazelas
sumber
8
Saya pikir BashPitfalls adalah sesuatu yang Anda akan sukai.
pawel7318
backlink dari artikel ini yang saya tulis , terima kasih atas
artikelnya
5
Saya ingin menyarankan menambahkan kelompok keempat: orang yang telah dipukul kepalanya untuk berlebihan mengutip terlalu banyak kali, mungkin oleh anggota kelompok ketiga mengambil frustrasi mereka pada orang lain (korban menjadi pengganggu). Yang menyedihkan tentu saja adalah bahwa mereka yang berasal dari kelompok keempat mungkin akhirnya gagal mengutip hal-hal yang paling penting.
ack

Jawaban:

201

Pembukaan

Pertama, saya katakan itu bukan cara yang tepat untuk mengatasi masalah tersebut. Ini seperti mengatakan " Anda tidak harus membunuh orang karena kalau tidak Anda akan masuk penjara ".

Demikian pula, Anda tidak mengutip variabel Anda karena jika tidak Anda memperkenalkan kerentanan keamanan. Anda mengutip variabel Anda karena itu salah untuk tidak (tetapi jika takut penjara dapat membantu, mengapa tidak).

Ringkasan kecil untuk mereka yang baru saja naik kereta.

Dalam kebanyakan shell, membiarkan ekspansi variabel tidak dikutip (meskipun itu (dan sisa jawaban ini) juga berlaku untuk substitusi perintah ( `...`atau $(...)) dan ekspansi aritmatika ( $((...))atau $[...])) memiliki arti yang sangat istimewa. Cara terbaik untuk mendeskripsikannya adalah seperti memanggil semacam operator + pembelahan implisit¹ .

cmd $var

dalam bahasa lain akan dituliskan sesuatu seperti:

cmd(glob(split($var)))

$varpertama-tama dipecah menjadi daftar kata sesuai dengan aturan kompleks yang melibatkan $IFSparameter khusus (bagian terbagi ) dan kemudian setiap kata yang dihasilkan dari pemisahan itu dianggap sebagai pola yang diperluas ke daftar file yang cocok dengan itu (bagian gumpalan ) .

Sebagai contoh, jika $varberisi *.txt,/var/*.xmldan $IFS mengandung ,, cmdakan dipanggil dengan sejumlah argumen, yang pertama adalah cmdyang berikutnya adalah txt file dalam direktori saat ini dan xmlfile dalam /var.

Jika Anda ingin menelepon cmdhanya dengan dua argumen literal cmd dan *.txt,/var/*.xml, Anda akan menulis:

cmd "$var"

yang akan menggunakan bahasa lain yang lebih Anda kenal:

cmd($var)

Apa yang kita maksud dengan kerentanan di shell ?

Bagaimanapun, sudah diketahui sejak awal waktu bahwa skrip shell tidak boleh digunakan dalam konteks sensitif-keamanan. Tentunya, OK, membiarkan variabel tidak dicantumkan adalah bug, tetapi itu tidak banyak merugikan, bukan?

Yah, terlepas dari kenyataan bahwa siapa pun akan memberi tahu Anda bahwa skrip shell tidak boleh digunakan untuk CGI web, atau untungnya sebagian besar sistem tidak mengizinkan skrip shell setuid / setgid saat ini, satu hal yang shellshock (bug bash yang dapat dieksploitasi dari jarak jauh yang membuat berita utama pada bulan September 2014) mengungkapkan bahwa shell masih digunakan secara luas di mana mereka seharusnya tidak boleh: di CGI, dalam skrip kait klien DHCP, dalam perintah sudoers, dipanggil oleh (jika bukan sebagai ) perintah setuid ...

Terkadang tanpa sadar. Sebagai contoh system('cmd $PATH_INFO') dalam skrip php/ perl/ pythonCGI memang memanggil shell untuk menafsirkan baris perintah itu (belum lagi fakta bahwa cmditu sendiri mungkin merupakan skrip shell dan penulisnya mungkin tidak pernah berharap akan dipanggil dari CGI).

Anda memiliki kerentanan ketika ada jalan untuk peningkatan hak istimewa, yaitu ketika seseorang (sebut saja penyerangnya ) dapat melakukan sesuatu yang tidak seharusnya.

Selalu itu berarti penyerang menyediakan data, bahwa data sedang diproses oleh pengguna / proses istimewa yang secara tidak sengaja melakukan sesuatu yang seharusnya tidak dilakukan, dalam sebagian besar kasus karena bug.

Pada dasarnya, Anda memiliki masalah ketika kode kereta Anda memproses data di bawah kendali penyerang .

Sekarang, tidak selalu jelas dari mana data itu berasal, dan seringkali sulit untuk mengetahui apakah kode Anda akan pernah memproses data yang tidak dipercaya.

Sejauh menyangkut variabel, Dalam kasus skrip CGI, cukup jelas, datanya adalah parameter GET / POST CGI dan hal-hal seperti cookie, path, host ... parameter.

Untuk skrip setuid (berjalan sebagai satu pengguna ketika dipanggil oleh yang lain), itu argumen atau variabel lingkungan.

Vektor lain yang sangat umum adalah nama file. Jika Anda mendapatkan daftar file dari direktori, ada kemungkinan file telah ditanam di sana oleh penyerang .

Dalam hal itu, bahkan pada prompt shell interaktif, Anda bisa rentan (saat memproses file di /tmpatau ~/tmp misalnya).

Bahkan a ~/.bashrcbisa rentan (misalnya, bashakan menafsirkannya ketika dipanggil sshuntuk menjalankan ForcedCommand seperti dalam gitpenyebaran server dengan beberapa variabel di bawah kendali klien).

Sekarang, skrip mungkin tidak dipanggil secara langsung untuk memproses data yang tidak dipercaya, tetapi skrip tersebut dapat dipanggil oleh perintah lain yang melakukannya. Atau kode yang salah Anda dapat disalin-salin ke skrip yang melakukannya (oleh Anda 3 tahun ke depan atau salah satu rekan Anda) Satu tempat yang sangat kritis adalah jawaban di situs Q&A karena Anda tidak akan pernah tahu di mana salinan kode Anda mungkin berakhir.

Turun ke bisnis; seberapa buruk?

Meninggalkan variabel (atau substitusi perintah) tanpa tanda kutip sejauh ini merupakan sumber kerentanan keamanan nomor satu yang terkait dengan kode shell. Sebagian karena bug itu sering diterjemahkan menjadi kerentanan tetapi juga karena sangat umum untuk melihat variabel yang tidak dikutip.

Sebenarnya, ketika mencari kerentanan dalam kode shell, hal pertama yang harus dilakukan adalah mencari variabel yang tidak dikutip. Mudah dikenali, seringkali kandidat yang baik, umumnya mudah dilacak kembali ke data yang dikendalikan penyerang.

Ada jumlah tak terbatas cara variabel berubah kutip bisa berubah menjadi kerentanan. Saya hanya akan memberikan beberapa tren umum di sini.

Pengungkapan informasi

Sebagian besar orang akan menabrak bug yang terkait dengan variabel yang tidak dikutip karena pembagian bagian (misalnya, sudah umum bagi file untuk memiliki ruang dalam nama mereka saat ini dan ruang dalam nilai default IFS). Banyak orang akan mengabaikan bagian gumpal . Bagian gumpalan setidaknya sama berbahayanya dengan bagian yang terbelah .

Gumpalan yang dilakukan atas input eksternal yang tidak disanitasi berarti penyerang dapat membuat Anda membaca konten direktori mana pun.

Di:

echo You entered: $unsanitised_external_input

jika $unsanitised_external_inputmengandung /*, itu berarti penyerang dapat melihat konten /. Bukan masalah besar. Ini menjadi lebih menarik meskipun dengan /home/*yang memberi Anda daftar nama pengguna pada mesin /tmp/*,, /home/*/.forwarduntuk petunjuk tentang praktik berbahaya lainnya, /etc/rc*/*untuk layanan yang diaktifkan ... Tidak perlu menyebutkan namanya secara individual. Nilai /* /*/* /*/*/*...hanya akan daftar seluruh sistem file.

Penolakan kerentanan layanan.

Mengambil kasus sebelumnya agak terlalu jauh dan kami punya DoS.

Sebenarnya, setiap variabel yang tidak dikutip dalam konteks daftar dengan input yang tidak bersih setidaknya adalah kerentanan DoS.

Bahkan penulis shell ahli biasanya lupa mengutip hal-hal seperti:

#! /bin/sh -
: ${QUERYSTRING=$1}

:adalah perintah no-op. Apa yang mungkin salah?

Yang dimaksudkan untuk menetapkan $1untuk $QUERYSTRINGjika $QUERYSTRING itu tidak diset. Itu cara cepat untuk membuat skrip CGI dapat dipanggil dari baris perintah juga.

Itu $QUERYSTRINGmasih diperluas dan karena tidak dikutip, operator split + glob dipanggil.

Sekarang, ada beberapa gumpalan yang sangat mahal untuk dikembangkan. Yang /*/*/*/*satu cukup buruk karena itu berarti daftar direktori hingga 4 level ke bawah. Selain aktivitas disk dan CPU, itu berarti menyimpan puluhan ribu path file (40k di sini di VM server minimal, 10k di antaranya direktori).

Sekarang /*/*/*/*/../../../../*/*/*/*berarti 40k x 10k dan /*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*cukup untuk membuat mesin terkuat sekalipun.

Cobalah sendiri (meskipun bersiaplah untuk mesin Anda crash atau hang):

a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'

Tentu saja, jika kodenya adalah:

echo $QUERYSTRING > /some/file

Kemudian Anda dapat mengisi disk.

Lakukan saja pencarian google di shell cgi atau bash cgi atau ksh cgi , dan Anda akan menemukan beberapa halaman yang menunjukkan cara menulis CGI dalam shell. Perhatikan bagaimana setengah dari mereka yang memproses parameter rentan.

Bahkan David Korn sendiri sangat rentan (lihat penanganan cookie).

hingga kerentanan eksekusi kode arbitrer

Eksekusi kode sewenang-wenang adalah jenis kerentanan terburuk, karena jika penyerang dapat menjalankan perintah apa pun, tidak ada batasan apa yang dapat ia lakukan.

Itu umumnya bagian split yang mengarah ke sana. Pemisahan itu menghasilkan beberapa argumen untuk diteruskan ke perintah ketika hanya satu yang diharapkan. Sementara yang pertama akan digunakan dalam konteks yang diharapkan, yang lain akan berada dalam konteks yang berbeda sehingga berpotensi ditafsirkan secara berbeda. Lebih baik dengan contoh:

awk -v foo=$external_input '$2 == foo'

Di sini, tujuannya adalah untuk menetapkan konten dari $external_inputvariabel shell ke foo awkvariabel.

Sekarang:

$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux

Kata kedua yang dihasilkan dari pemisahan $external_input tidak ditugaskan footetapi dianggap sebagai awkkode (di sini yang mengeksekusi perintah arbitrer:) uname.

Itu terutama masalah bagi perintah yang dapat mengeksekusi perintah lain ( awk, env, sed(GNU satu), perl, find...) terutama dengan varian GNU (yang menerima pilihan setelah argumen). Kadang-kadang, Anda tidak akan menduga perintah untuk dapat mengeksekusi orang lain seperti ksh, bashatau zsh's [atau printf...

for file in *; do
  [ -f $file ] || continue
  something-that-would-be-dangerous-if-$file-were-a-directory
done

Jika kita membuat direktori bernama x -o yes, maka tes menjadi positif, karena itu adalah ekspresi kondisional yang sama sekali berbeda yang kami evaluasi.

Lebih buruk lagi, jika kita membuat file bernama x -a a[0$(uname>&2)] -gt 1, dengan semua implementasi ksh setidaknya (yang mencakup sh sebagian besar Unices komersial dan beberapa BSD), yang dijalankan uname karena shell tersebut melakukan evaluasi aritmatika pada operator perbandingan numerik dari [perintah.

$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux

Sama dengan bashuntuk nama file seperti x -a -v a[0$(uname>&2)].

Tentu saja, jika mereka tidak bisa mendapatkan eksekusi sewenang-wenang, penyerang dapat menerima kerusakan yang lebih kecil (yang dapat membantu untuk mendapatkan eksekusi sewenang-wenang). Perintah apa pun yang dapat menulis file atau mengubah izin, kepemilikan atau memiliki efek utama atau samping dapat dieksploitasi.

Segala macam hal dapat dilakukan dengan nama file.

$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done

Dan Anda akhirnya ..dapat menulis (secara rekursif dengan GNU chmod).

Skrip yang melakukan pemrosesan file secara otomatis di area /tmpyang dapat ditulis untuk umum harus ditulis dengan sangat hati-hati.

Bagaimana dengan [ $# -gt 1 ]

Itu sesuatu yang saya temukan menjengkelkan. Beberapa orang turun semua kesulitan bertanya-tanya apakah ekspansi tertentu mungkin bermasalah untuk memutuskan apakah mereka dapat menghilangkan tanda kutip.

Ini seperti mengatakan. Hei, sepertinya $#tidak bisa dikenai operator split + glob, mari kita minta shell untuk membagi + glob itu . Atau Hei, mari kita menulis kode yang salah hanya karena bug tidak mungkin terkena .

Sekarang seberapa tidak mungkin itu? OKE, $#(atau $!, $?atau substitusi aritmatika apa pun) hanya boleh berisi angka (atau -untuk sebagian) sehingga bagian gumpalan keluar. Untuk bagian yang terbagi untuk melakukan sesuatu, yang kita butuhkan hanyalah $IFSmemuat angka (atau -).

Dengan beberapa cangkang, $IFSmungkin diwarisi dari lingkungan, tetapi jika lingkungan tidak aman, permainan akan berakhir.

Sekarang jika Anda menulis fungsi seperti:

my_function() {
  [ $# -eq 2 ] || return
  ...
}

Artinya adalah bahwa perilaku fungsi Anda tergantung pada konteksnya. Atau dengan kata lain, $IFS menjadi salah satu masukan untuk itu. Sebenarnya, ketika Anda menulis dokumentasi API untuk fungsi Anda, itu harus seperti:

# my_function
#   inputs:
#     $1: source directory
#     $2: destination directory
#   $IFS: used to split $#, expected not to contain digits...

Dan kode yang memanggil fungsi Anda perlu memastikan $IFStidak mengandung digit. Semua itu karena Anda tidak ingin mengetik 2 karakter kutipan ganda itu.

Sekarang, agar [ $# -eq 2 ]bug itu menjadi kerentanan, Anda perlu entah bagaimana agar nilainya $IFSmenjadi di bawah kendali penyerang . Dapat dibayangkan, itu tidak akan terjadi kecuali penyerang berhasil mengeksploitasi bug lain.

Tapi itu tidak pernah terdengar. Kasus yang umum adalah ketika orang lupa membersihkan data sebelum menggunakannya dalam ekspresi aritmatika. Kita telah melihat di atas bahwa itu dapat memungkinkan eksekusi kode arbitrer di beberapa shell, tetapi di semua itu, memungkinkan penyerang untuk memberikan variabel nilai integer.

Misalnya:

n=$(($1 + 1))
if [ $# -gt 2 ]; then
  echo >&2 "Too many arguments"
  exit 1
fi

Dan dengan $1nilai (IFS=-1234567890), evaluasi aritmatika tersebut memiliki efek samping dari pengaturan IFS dan [ perintah berikutnya gagal yang berarti pemeriksaan untuk terlalu banyak argumen dilewati.

Bagaimana dengan ketika operator split + glob tidak dipanggil?

Ada kasus lain di mana tanda kutip diperlukan di sekitar variabel dan ekspansi lainnya: ketika itu digunakan sebagai pola.

[[ $a = $b ]]   # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac

jangan menguji apakah $adan $bsama (kecuali dengan zsh) tetapi jika $acocok dengan pola di $b. Dan Anda perlu untuk mengutip $bjika Anda ingin membandingkan sebagai string (hal yang sama di "${a#$b}"atau "${a%$b}"atau "${a##*$b*}"di mana $bharus dikutip jika tidak diambil sebagai pola).

Apa itu artinya bahwa [[ $a = $b ]]mungkin kembali benar dalam kasus di mana $aberbeda dari $b(misalnya ketika $aadalah anythingdan $badalah *) atau dapat kembali palsu ketika mereka identik (misalnya ketika kedua $adan $byang [a]).

Bisakah itu membuat kerentanan keamanan? Ya, seperti bug apa pun. Di sini, penyerang dapat mengubah alur kode logis skrip Anda dan / atau mematahkan asumsi yang dibuat skrip Anda. Misalnya, dengan kode seperti:

if [[ $1 = $2 ]]; then
   echo >&2 '$1 and $2 cannot be the same or damage will incur'
   exit 1
fi

Penyerang dapat melewati cek dengan melewati '[a]' '[a]'.

Sekarang, jika tidak ada pencocokan pola atau operator glob + split , apa bahaya meninggalkan variabel yang tidak dikutip?

Saya harus mengakui bahwa saya memang menulis:

a=$b
case $a in...

Di sana, mengutip tidak membahayakan tetapi tidak sepenuhnya diperlukan.

Namun, satu efek samping dari menghilangkan kuotasi dalam kasus-kasus tersebut (misalnya dalam jawaban Q&A) adalah bahwa ia dapat mengirim pesan yang salah kepada para pemula: bahwa boleh saja tidak mengutip variabel .

Sebagai contoh, mereka mungkin mulai berpikir bahwa jika a=$bOK, maka export a=$bakan baik (yang tidak dalam banyak shell seperti dalam argumen ke exportperintah sehingga dalam konteks daftar) atau env a=$b.

Bagaimana dengan zsh?

zshmemang memperbaiki sebagian besar canggung desain. Dalam zsh(setidaknya ketika tidak dalam mode emulasi sh / ksh), jika Anda ingin membelah , atau menggumpal , atau mencocokkan pola , Anda harus memintanya secara eksplisit: $=varuntuk membelah, dan $~varuntuk menggumpal atau agar isi dari variabel diperlakukan sebagai sebuah pola.

Namun, pemisahan (tetapi tidak globbing) masih dilakukan secara implisit pada substitusi perintah yang tidak dikutip (seperti dalam echo $(cmd)).

Juga, efek samping yang kadang-kadang tidak diinginkan dari tidak mengutip variabel adalah penghapusan kosong . The zshperilaku ini mirip dengan apa yang dapat Anda capai dalam kerang lainnya dengan menonaktifkan globbing sama sekali (dengan set -f) dan membelah (dengan IFS=''). Tetap:

cmd $var

Tidak akan ada split + glob , tetapi jika $varkosong, alih-alih menerima satu argumen kosong, tidak cmdakan menerima argumen sama sekali.

Itu bisa menyebabkan bug (seperti yang sudah jelas [ -n $var ]). Itu mungkin dapat mematahkan harapan dan asumsi skrip dan menyebabkan kerentanan, tetapi saya tidak dapat memberikan contoh yang tidak terlalu jauh-baru saja diambil).

Bagaimana bila Anda lakukan membutuhkan perpecahan + gumpal Operator?

Ya, itu biasanya ketika Anda ingin membiarkan variabel Anda tidak dikutip. Tetapi kemudian Anda perlu memastikan Anda menyetel split dan operator glob Anda dengan benar sebelum menggunakannya. Jika Anda hanya ingin bagian split dan bukan bagian glob (yang merupakan kasus sebagian besar waktu), maka Anda perlu menonaktifkan globbing ( set -o noglob/ set -f) dan memperbaikinya $IFS. Kalau tidak, Anda akan menyebabkan kerentanan juga (seperti contoh CGI David Korn yang disebutkan di atas).

Kesimpulan

Singkatnya, meninggalkan variabel (atau perintah substitusi atau ekspansi aritmatika) tanpa tanda kutip di shell bisa sangat berbahaya memang terutama ketika dilakukan dalam konteks yang salah, dan sangat sulit untuk mengetahui mana konteks yang salah itu.

Itulah salah satu alasan mengapa itu dianggap praktik yang buruk .

Terima kasih sudah membaca sejauh ini. Jika itu melampaui kepala Anda, jangan khawatir. Orang tidak dapat mengharapkan semua orang memahami semua implikasi dari penulisan kode mereka seperti cara mereka menulisnya. Itu sebabnya kami memiliki rekomendasi praktik yang baik , sehingga dapat diikuti tanpa harus memahami mengapa.

(dan jika itu belum jelas, harap hindari menulis kode sensitif keamanan dalam shells).

Dan silahkan kutipan variabel Anda pada jawaban Anda di situs ini!


¹Dalam ksh93dan pdkshdan turunannya, ekspansi brace juga dilakukan kecuali globbing dinonaktifkan (dalam kasus ksh93versi hingga ksh93u +, bahkan ketika braceexpandopsi dinonaktifkan).

Stéphane Chazelas
sumber
Perhatikan bahwa, dengan [[, hanya perbandingan RHS yang perlu dikutip:if [[ $1 = "$2" ]]; then
mirabilos
2
@mirabilos, ya, tapi LHS kebutuhan tidak tidak dikutip, jadi tidak ada alasan kuat untuk tidak mengutip sana (jika kita mengambil keputusan sadar untuk mengutip secara default karena tampaknya menjadi hal yang paling masuk akal untuk dilakukan ). Perhatikan juga bahwa [[ $* = "$var" ]]tidak sama dengan [[ "$*" = "$var" ]]jika karakter pertama $IFSbukan spasi dengan bash(dan juga mkshjika $IFSkosong meskipun dalam kasus itu saya tidak yakin apa $*yang sama dengan, haruskah saya menaikkan bug?)).
Stéphane Chazelas
1
Ya, Anda dapat mengutip secara default di sana. Harap tidak ada lagi bug tentang pemisahan bidang sekarang, saya masih harus memperbaiki yang saya tahu (dari Anda dan orang lain) terlebih dahulu, sebelum kita dapat mengevaluasi kembali ini.
mirabilos
2
@Barmar, dengan asumsi Anda bermaksud foo='bar; rm *', tidak, tidak akan, itu akan tetapi daftar isi direktori saat ini yang dapat dihitung sebagai pengungkapan informasi. print $foodi ksh93( di mana printpenggantian untuk echoyang mengatasi beberapa kekurangannya) memang memiliki kerentanan injeksi kode (misalnya dengan foo='-f%.0d z[0$(uname>&2)]') (Anda benar-benar perlu print -r -- "$foo". echo "$foo"masih salah dan tidak dapat diperbaiki (meskipun umumnya kurang berbahaya)).
Stéphane Chazelas
3
Saya bukan ahli bash, tapi saya sudah mengkodekannya selama lebih dari satu dekade. Saya banyak menggunakan kutipan, tetapi terutama untuk menangani bagian yang kosong. Sekarang, saya akan lebih banyak menggunakannya! Akan lebih baik jika seseorang memperluas jawaban ini untuk membuatnya sedikit lebih mudah menyerap semua poin yang bagus. Saya mendapat banyak hal, tetapi saya juga kehilangan banyak hal. Sudah posting yang panjang, tapi saya tahu ada banyak lagi di sini untuk saya pelajari. Terima kasih!
Joe
34

[Terinspirasi oleh jawaban ini oleh cas .]

Tetapi bagaimana jika ...?

Tetapi bagaimana jika skrip saya menetapkan variabel ke nilai yang diketahui sebelum menggunakannya? Secara khusus, bagaimana jika ia menetapkan variabel ke salah satu dari dua atau lebih nilai yang mungkin (tetapi selalu menetapkannya untuk sesuatu yang diketahui), dan tidak ada nilai yang mengandung spasi atau karakter gumpalan? Tidakkah aman menggunakannya tanpa tanda kutip dalam kasus itu ?

Dan bagaimana jika salah satu nilai yang mungkin adalah string kosong, dan saya bergantung pada "penghapusan kosong"? Yaitu, jika variabel berisi string kosong, saya tidak ingin mendapatkan string kosong di perintah saya; Saya tidak ingin mendapat apa-apa. Sebagai contoh,

jika some_condition
kemudian
    ignorecase = "- i"
lain
    ignorecase = ""
fi
                                        # Perhatikan bahwa tanda kutip dalam perintah di atas tidak sepenuhnya diperlukan. 
grep $ ignorecase   other_ grep _args

Saya tidak bisa mengatakan ; itu akan gagal jika string kosong.grep "$ignorecase" other_grep_args$ignorecase

Tanggapan:

Seperti dibahas dalam jawaban lain, ini masih akan gagal jika IFSberisi a -atau a i. Jika Anda memastikan bahwa IFStidak mengandung karakter apa pun di variabel Anda (dan Anda yakin bahwa variabel Anda tidak mengandung karakter gumpalan), maka ini mungkin aman.

Tetapi ada cara yang lebih aman (meskipun agak jelek dan sangat tidak intuitif): gunakan ${ignorecase:+"$ignorecase"}. Dari spesifikasi Bahasa Perintah POSIX Shell , di bawah  2.6.2 Parameter Ekspansi ,

${parameter:+[word]}

    Gunakan Nilai Alternatif.   Jika parametertidak disetel atau nol, null harus diganti; jika tidak, ekspansi word (atau string kosong jika worddihilangkan) harus diganti.

Kuncinya di sini, seperti apa adanya, adalah bahwa kita menggunakan ignorecasesebagai parameter dan "$ignorecase"sebagai word. Jadi ${ignorecase:+"$ignorecase"}artinya

Jika $ignorecasetidak disetel atau nol (yaitu, kosong), nol (yaitu, tidak ada tanda kutip ) harus diganti; jika tidak, perluasan "$ignorecase"harus diganti.

Ini membawa kita ke tempat yang kita inginkan: jika variabel diatur ke string kosong, itu akan "dihapus" (seluruh ekspresi berbelit-belit ini tidak akan mengevaluasi apa pun - bahkan string kosong), dan jika variabel tersebut memiliki -Nilai kosong, kami mendapatkan nilai itu, dikutip.


Tetapi bagaimana jika ...?

Tetapi bagaimana jika saya memiliki variabel yang ingin / perlu saya bagi menjadi kata-kata? (Ini sebaliknya seperti kasus pertama; skrip saya telah menetapkan variabel, dan saya yakin itu tidak mengandung karakter gumpal. Tetapi mungkin berisi spasi, dan saya ingin membaginya menjadi argumen terpisah di ruang tersebut batas-batas.
PS Saya masih ingin mengosongkan penghapusan.)

Sebagai contoh,

jika some_condition
kemudian
    kriteria = "- ketik f"
lain
    kriteria = ""
fi
jika some_other_condition
kemudian
    criteria = "$ criteria -mtime +42"
fi
temukan "$ start_directory" $ criteria   other_ find _args

Tanggapan:

Anda mungkin berpikir bahwa ini adalah kasus untuk digunakan eval.  Tidak!   Tahan godaan untuk berpikir untuk menggunakan di evalsini.

Sekali lagi, jika Anda telah memastikan bahwa IFStidak mengandung karakter apa pun dalam variabel Anda (kecuali untuk spasi, yang Anda ingin dihormati), dan Anda yakin bahwa variabel Anda tidak mengandung karakter gumpalan, maka di atas mungkin adalah aman.

Tapi, jika Anda menggunakan bash (atau ksh, zsh atau yash), ada cara yang lebih aman: gunakan array:

jika some_condition
kemudian
    criteria = (- type f) # Anda bisa mengatakan `criteria = (" - type "" f ")`, tetapi itu benar-benar tidak perlu.
lain
    criteria = () # Jangan gunakan tanda kutip pada perintah ini!
fi
jika some_other_condition
kemudian
    criteria + = (- mtime +42) # Catatan: bukan `=`, tetapi ` + =`, untuk menambahkan (menambahkan) ke array.
fi
temukan "$ start_directory" "$ {criteria [@]}"   other_ find _args

Dari bash (1) ,

Elemen array dapat dirujuk menggunakan . ... Jika ini atau  , kata mengembang untuk semua anggota . Subskrip ini berbeda hanya ketika kata itu muncul dalam tanda kutip ganda. Jika kata tersebut dikutip ganda, ... perluas setiap elemen menjadi kata yang terpisah.${name[subscript]}subscript@*name${name[@]}name

Jadi "${criteria[@]}"memperluas (dalam contoh di atas) elemen nol, dua, atau empat criteriaarray, masing-masing dikutip. Secara khusus, jika tidak ada kondisi  s yang benar, criteriaarray tidak memiliki konten (seperti yang ditetapkan oleh criteria=()pernyataan), dan tidak "${criteria[@]}"mengevaluasi apa - apa (bahkan string kosong yang tidak nyaman).


Ini menjadi sangat menarik dan rumit ketika Anda berurusan dengan beberapa kata, beberapa di antaranya adalah input dinamis (pengguna), yang sebelumnya tidak Anda ketahui, dan mungkin berisi spasi atau karakter khusus lainnya. Mempertimbangkan:

printf "Masukkan nama file untuk dicari:"
baca fname
jika ["$ fname"! = ""]
kemudian
    kriteria + = (- nama "$ fname")
fi

Catatan yang $fnamedikutip setiap kali digunakan. Ini berfungsi bahkan jika pengguna memasukkan sesuatu seperti foo baratau foo*"${criteria[@]}"mengevaluasi ke -name "foo bar"atau -name "foo*". (Ingat bahwa setiap elemen array dikutip.)

Array tidak berfungsi di semua shell POSIX; array adalah ksh / bash / zsh / yash-ism. Kecuali ... ada satu array yang didukung semua shell: daftar argumen, alias "$@". Jika Anda selesai dengan daftar argumen yang Anda panggil (misalnya, Anda telah menyalin semua "parameter posisi" (argumen) ke dalam variabel, atau memprosesnya), Anda dapat menggunakan daftar arg sebagai array:

jika some_condition
kemudian
    set - -type f # Anda bisa mengatakan `set -" -type "" f "`, tapi itu benar-benar tidak perlu.
lain
    mengatur -
fi
jika some_other_condition
kemudian
    set - "$ @" -mtime +42
fi
# Demikian pula: set - "$ @" -name "$ fname"
temukan "$ start_directory" "$ @"   other_ find _args

The "$@"konstruk (yang, secara historis, datang pertama) memiliki semantik yang sama seperti - mengembang setiap argumen (yaitu, setiap elemen dari daftar argumen) untuk sebuah kata yang terpisah, seperti jika Anda telah mengetik ."${name[@]}""$1" "$2" "$3" …

Mengutip dari spesifikasi Bahasa Perintah POSIX Shell , di bawah 2.5.2 Parameter Khusus ,

@

    Perluas ke parameter posisi, mulai dari satu, awalnya menghasilkan satu bidang untuk setiap parameter posisi yang ditetapkan. …, Bidang awal harus dipertahankan sebagai bidang terpisah,…. Jika tidak ada parameter posisi, perluasan @harus menghasilkan bidang nol, bahkan ketika @berada dalam tanda kutip ganda; ...

Teks lengkapnya agak samar; titik kuncinya adalah bahwa ia menentukan yang "$@"akan menghasilkan bidang nol ketika tidak ada parameter posisi. Catatan sejarah: ketika "$@"pertama kali diperkenalkan di shell Bourne (pendahulu untuk bash) pada tahun 1979, ia memiliki bug yang "$@"digantikan oleh string kosong tunggal ketika tidak ada parameter posisi; lihat Apa ${1+"$@"}artinya dalam skrip shell, dan bagaimana bedanya "$@"? Keluarga Tradisional Bourne ShellApa ${1+"$@"}artinya ... dan di mana perlu? , dan "$@"versus${1+"$@"} .


Array membantu dengan situasi pertama, juga:

jika some_condition
kemudian
    ignorecase = (- i) # Anda bisa mengatakan `ignorecase = (" - i ")`, tetapi itu benar-benar tidak perlu.
lain
    ignorecase = () # Jangan gunakan tanda kutip pada perintah ini!
fi
grep "$ {ignorecase [@]}"   other_ grep _args

____________________

PS (csh)

Ini harus dilakukan tanpa berkata, tetapi, untuk kepentingan orang-orang yang baru di sini: csh, tcsh, dll., Bukan cangkang Bourne / POSIX. Mereka keluarga yang sangat berbeda. Seekor kuda dengan warna berbeda. Seluruh permainan bola lainnya. Jenis kucing yang berbeda. Burung dari bulu lain. Dan, paling khusus, kaleng cacing yang berbeda.

Beberapa dari apa yang dikatakan di halaman ini berlaku untuk csh; seperti: itu adalah ide yang baik untuk mengutip semua variabel Anda kecuali Anda memiliki alasan yang baik untuk tidak melakukannya, dan Anda yakin Anda tahu apa yang Anda lakukan. Tapi, di csh, setiap variabel adalah array - Kebetulan hampir setiap variabel adalah array hanya dari satu elemen, dan bertindak sangat mirip dengan variabel shell biasa di shell Bourne / POSIX. Dan sintaksnya sangat berbeda (dan maksud saya sangat buruk ). Jadi kita tidak akan mengatakan apa-apa lagi tentang csh-family shells di sini.

G-Man
sumber
1
Perhatikan bahwa dalam csh, Anda lebih suka menggunakan $var:qdaripada "$var"yang terakhir tidak berfungsi untuk variabel yang berisi karakter baris baru (atau jika Anda ingin mengutip elemen-elemen array secara individual, daripada bergabung dengan spasi dengan argumen tunggal).
Stéphane Chazelas
Di bash, bitrate="--bitrate 320"bekerja dengan ${bitrate:+$bitrate}, dan bitrate=--bitrate 128tidak akan bekerja ${bitrate:+"$bitrate"}karena itu melanggar perintah. Apakah aman digunakan ${variable:+$variable}tanpa "?
Freedo
@Freedo: Saya menemukan komentar Anda tidak jelas. Saya khususnya tidak jelas apa yang ingin Anda ketahui yang belum saya katakan. Lihat judul "Tetapi bagaimana jika" yang kedua - "Tetapi bagaimana jika saya memiliki variabel yang saya ingin / perlu pisahkan menjadi kata-kata?" Itulah situasi Anda, dan Anda harus mengikuti saran di sana. Tetapi jika Anda tidak bisa (misalnya, karena Anda menggunakan shell selain bash, ksh, zsh atau yash, dan Anda menggunakan  $@sesuatu yang lain), atau Anda hanya menolak menggunakan array karena alasan lain, saya merujuk Anda untuk "Seperti yang dibahas dalam jawaban lain". ... (Lanjutan)
G-Man
(Lanjutan) ... Saran Anda (menggunakan ${variable:+$variable}tanpa ") akan gagal jika IFS mengandung  -,  0, 2, 3, a, b, e, i, ratau  t.
G-Man
Yah variabel saya memiliki karakter a, e, b, i 2 dan 3 dan masih berfungsi dengan baik${bitrate:+$bitrate}
Freedo
11

Saya ragu dengan jawaban Stéphane, namun ada kemungkinan untuk menyalahgunakan $#:

$ set `seq 101`

$ IFS=0

$ echo $#
1 1

atau $ ?:

$ IFS=0

$ awk 'BEGIN {exit 101}'

$ echo $?
1 1

Ini adalah contoh yang dibuat-buat, tetapi potensi memang ada.

Steven Penny
sumber