Kami mencari bug dalam kode PHP tanpa penganalisa statis

Bagian favorit saya dalam analisis kode statis adalah mengajukan hipotesis tentang potensi kesalahan dalam kode dan kemudian memeriksanya.


Contoh hipotesis:


 strpos      . 

Tetapi ada kemungkinan bahwa bahkan pada beberapa juta baris kode diagnosis seperti itu tidak akan "menembak", jadi Anda tidak ingin menghabiskan banyak waktu pada hipotesis yang tidak berhasil.


Hari ini saya akan menunjukkan cara melakukan analisis statis paling sederhana menggunakan utilitas phpgrep tanpa menulis kode.



Di bawah potongan:


  • Cari dan analisis bug dalam proyek sumber terbuka.
  • Mulai cepat dengan phpgrep.
  • Prinsip pencarian sintaksis.




Latar belakang


Selama beberapa bulan sekarang, saya telah mendukung PHP NoVerify linter (baca di artikel NoVerify: Linter for PHP dari Tim VKontakte ).


Dari waktu ke waktu, ide untuk diagnostik baru muncul di tim. Mungkin ada banyak ide, tetapi saya ingin memeriksa semuanya, terutama jika pemeriksaan yang diusulkan ditujukan untuk mengidentifikasi cacat kritis.


Sebelumnya, saya aktif mengembangkan go-kritik dan situasinya mirip, dengan satu-satunya perbedaan adalah bahwa kode sumber dianalisis dalam Go, dan bukan dalam PHP. Ketika saya mengetahui tentang utilitas gogrep , dunia saya terbalik. Seperti namanya, utilitas ini memiliki kesamaan dengan grep, hanya pencarian yang dilakukan bukan dengan ekspresi reguler, tetapi oleh pola sintaks (saya akan menjelaskan nanti apa artinya).


Saya tidak ingin hidup tanpa grep yang pintar, jadi suatu malam saya memutuskan untuk duduk dan menulis phpgrep .


Kasus yang dianalisis


Agar menyenangkan, kami segera membenamkan diri dalam aplikasi. Kami akan menganalisis satu set kecil proyek PHP yang cukup terkenal dan besar yang tersedia di GitHub.


Kit kami mencakup proyek-proyek berikut:



Bagi orang-orang yang merencanakan apa yang kita rencanakan, ini adalah set yang sangat membangkitkan selera.


Jadi ayo pergi!


Menggunakan Penugasan sebagai Ekspresi


Jika penugasan digunakan sebagai ekspresi, apalagi:


  • konteks mengharapkan hasil operasi logis (kondisi logis) dan
  • sisi kanan ekspresi tidak memiliki efek samping dan konstan,

kemungkinan besar kesalahan dalam kode.


Untuk memulainya, mari kita ambil konstruksi berikut untuk "konteks logis":


  1. Ekspresi di dalam " if ($cond) ".
  2. Kondisi operator ternary adalah: " $cond ? $x : $y ".
  3. Kondisi kelanjutan untuk loop " while ($cond) " dan " for ($init; $cond; $post) ".

Di sisi kanan penugasan, kami mengharapkan konstanta atau literal.


Mengapa kita membutuhkan batasan seperti itu?
Kedua pembatasan diperlukan untuk mengurangi jumlah positif palsu.

Jika kita mencari penugasan secara eksklusif dalam kondisi, maka probabilitas bahwa ada kesalahan ketik dan bukannya "=" tersirat oleh "==" lebih tinggi.

Tetapi bahkan dalam kasus ini, situasi kontroversial tetap di mana tugas masuk akal karena efek samping yang mungkin terjadi dalam suatu fungsi. Agar tidak berurusan dengan mereka, kami hanya akan memilih nilai yang diberikan paling sederhana.

Mari kita mulai dengan (1):


 #      # |      (  ) # | | # | | phpgrep . 'if ($_ = []) $_' # 1 # | # | #    #  3    . phpgrep . 'if ($_ = ${"const"}) $_' # 2 phpgrep . 'if ($_ = ${"str"}) $_' # 3 phpgrep . 'if ($_ = ${"num"}) $_' # 4 

Di sini kita melihat 4 pola, satu-satunya perbedaan di antaranya adalah ekspresi yang ditetapkan (RHS). Mari kita mulai dengan yang pertama.


Template " if ($_ = []) $_ " menangkap if , yang memiliki larik kosong yang ditetapkan untuk ekspresi apa pun. $_ cocok dengan ekspresi atau pernyataan apa pun.


     (RHS) | if ($_ = []) $_ | | |   if',   ,  {}    LHS   

Contoh berikut menggunakan grup const , str, dan num yang lebih kompleks. Tidak seperti $_ mereka menggambarkan batasan pada operasi yang kompatibel.


  • const adalah konstanta bernama atau konstanta kelas.
  • str adalah string literal dari jenis apa pun.
  • num adalah literal numerik dari jenis apa pun.

Pola-pola ini cukup untuk mencapai beberapa operasi pada kasus ini.


βŽ† moodle / blok / rss_client / viewfeed.php # L37 :


 if ($courseid = SITEID) { $courseid = 0; } 

Pemicu kedua dalam moodle adalah ketergantungan ADOdb . Di perpustakaan hulu, masalahnya masih ada.


βŽ† ADOdb / driver / adodb-odbtp.inc.php # L741 :



Ada banyak fragmen ini, tetapi bagi kami hanya baris pertama yang relevan. Alih-alih membandingkan bidang databaseType , kami melakukan tugas dan selalu masuk ke dalam kondisi.


Tempat lain yang menarik di mana kami ingin melakukan tindakan hanya untuk catatan "yang benar", tetapi sebaliknya, selalu jalankan dan, lebih lagi, tandai semua catatan sebagai benar!


βŽ† moodle / pertanyaan / format / blackboard_six / formatqti.php # L598 :


 // For BB Fill in the Blank, only interested in correct answers. if ($response->feedback = 'correct') { // ... } 

Daftar templat yang diperluas untuk pemeriksaan ini
  phpgrep.  'untuk ($ _; $ _ = []; $ _) $ _'
 phpgrep.  'untuk ($ _; $ _ = $ {"const"}; $ _) $ _'
 phpgrep.  'untuk ($ _; $ _ = $ {"num"}; $ _) $ _'
 phpgrep.  'untuk ($ _; $ _ = $ {"str"}; $ _) $ _'
 phpgrep.  'while ($ _ = []) $ _'
 phpgrep.  'while ($ _ = $ {"const"}) $ _'
 phpgrep.  'while ($ _ = $ {"num"}) $ _'
 phpgrep.  'while ($ _ = $ {"str"}) $ _'
 phpgrep.  'if ($ _ = []) $ _'
 phpgrep.  'if ($ _ = $ {"const"}) $ _'
 phpgrep.  'if ($ _ = $ {"str"}) $ _'
 phpgrep.  'if ($ _ = $ {"num"}) $ _'
 phpgrep.  '$ _ = []?  $ _: $ _ '
 phpgrep.  '$ _ = $ {"const"}?  $ _: $ _ '
 phpgrep.  '$ _ = $ {"str"}?  $ _: $ _ '
 phpgrep.  '$ _ = $ {"num"}?  $ _: $ _ '
 phpgrep.  '($ _ = []) && $ _'
 phpgrep.  '($ _ = $ {"const"}) && $ _'
 phpgrep.  '($ _ = $ {"str"}) && $ _'
 phpgrep.  '($ _ = $ {"num"}) && $ _'
 phpgrep.  '$ _ && $ _ = []'
 phpgrep.  '$ _ && $ _ = $ {"const"}'
 phpgrep.  '$ _ && $ _ = $ {"str"}'
 phpgrep.  '$ _ && $ _ = $ {"num"}'
 phpgrep.  '($ _ = []) ||  $ _ '
 phpgrep.  '($ _ = $ {"const"}) ||  $ _ '
 phpgrep.  '($ _ = $ {"str"}) ||  $ _ '
 phpgrep.  '($ _ = $ {"num"}) ||  $ _ '
 phpgrep.  '$ _ ||  $ _ = [] '
 phpgrep.  '$ _ ||  $ _ = $ {"const"} '
 phpgrep.  '$ _ ||  $ _ = $ {"str"} '
 phpgrep.  '$ _ ||  $ _ = $ {"num"} ' 

Mari kita ulangi apa yang kita pelajari:


  • Template terlihat seperti kode php yang mereka temukan.
  • $_ berarti apa saja. Anda dapat membandingkan dengan . dalam ekspresi reguler.
  • ${"<class>"} bekerja seperti $_ dengan batasan tipe elemen AST.

Perlu juga ditekankan bahwa segala sesuatu kecuali variabel dipetakan secara harfiah. Ini berarti bahwa pola " array(1, 2 + 3) " akan dipenuhi hanya oleh kode yang identik dalam struktur sintaksis (spasi tidak mempengaruhi). Di sisi lain, pola " array($_, $_) " memenuhi literal array dua elemen.


Membandingkan ekspresi dengan dirimu sendiri


Kebutuhan untuk membandingkan sesuatu dengan diri sendiri sangat jarang. Ini mungkin cek NaN , tapi setidaknya separuh waktu itu merupakan kesalahan salin / tempel.


βŽ† Wikia / aplikasi / ekstensi / SemanticDrilldown / termasuk / SD_FilterValue.php # L103 :


 if ( $fv1->month == $fv1->month ) return 0; 

Ke kanan harus " $fv2->month ".


Untuk mengekspresikan bagian duplikat dalam templat, kami menggunakan variabel dengan nama selain " _ ". Mekanisme pengulangan dalam suatu pola mirip dengan backlink dalam ekspresi reguler.


Pola " $x == $x " akan sesuai dengan yang ditemukan oleh contoh di atas. Alih-alih " x ", nama apa pun dapat digunakan. Yang penting nama-nama itu identik. Variabel templat yang memiliki nama yang dibedakan tidak harus memiliki konten yang sama saat mengambil.


Contoh berikut ditemukan menggunakan " $x <= $x ".


βŽ† Drupal / inti / modul / tampilan / tes / src / Unit / ViewsDataTest.php # L166 :


 $prev = $base_tables[$base_tables_keys[$i - 1]]; $current = $base_tables[$base_tables_keys[$i]]; $this->assertTrue( $prev['weight'] <= $current['weight'] && $prev['title'] <= $prev['title'], // <--------------  'The tables are sorted as expected.'); 

Subekspresi duplikat


Sekarang kita tahu tentang kemungkinan subekspresi berulang, kita dapat menyusun banyak pola yang menarik.


Salah satu favorit saya adalah " $_ ? $x : $x ".
Ini adalah operator ternary dengan cabang benar / salah identik.


βŽ† joomla-cms / libraries / src / User / UserHelper.php # L522 :


 return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; 

Kedua cabang digandakan, yang menunjukkan potensi masalah dalam kode. Jika kita melihat kode di sekitar, kita bisa mengerti apa yang seharusnya. Demi keterbacaan, saya memotong bagian dari kode dan mengurangi nama variabel $encrypted menjadi $enc .


 case 'crypt-blowfish': return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); case 'md5-base64': return ($show_encrypt) ? '{MD5}' . $enc : $enc; case 'ssha': return ($show_encrypt) ? '{SSHA}' . $enc : $enc; case 'smd5': return ($show_encrypt) ? '{SMD5}' . $enc : $enc; case 'sha256': return ($show_encrypt) ? '{SHA256}' . $enc : '{SHA256}' . $enc; default: return ($show_encrypt) ? '{MD5}' . $enc : $enc; 

Saya berani bertaruh bahwa kode membutuhkan tambalan berikut:


 - ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted; 

Prioritas Operasi Berbahaya dalam PHP


Tindakan pencegahan yang baik dalam PHP adalah penggunaan tanda kurung pengelompokan di mana pun penting untuk memiliki urutan perhitungan yang benar.


Dalam banyak bahasa pemrograman, ungkapan " x & mask != 0 " memiliki makna intuitif. Jika mask menggambarkan sedikit, maka kode ini memeriksa bahwa pada x bit ini tidak sama dengan nol. Sayangnya, untuk PHP ungkapan ini akan dihitung seperti ini: " x & (mask != 0) ", yang hampir selalu bukan yang Anda butuhkan.


WordPress, Joomla dan moodle menggunakan SimplePie .


βŽ† SimplePie / library / SimplePie / Locator.php # L254
βŽ† SimplePie / library / SimplePie / Locator.php # L384
βŽ† SimplePie / library / SimplePie / Locator.php # L412
βŽ† SimplePie / library / SimplePie / Sanitize.php # L349
βŽ† SimplePie / library / SimplePie.php # L1634


 $feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0 

SIMPLEPIE_FILE_SOURCE_REMOTE didefinisikan sebagai 1 , sehingga ekspresi akan sama dengan:


 $feed->method & (1 === 0) // => $feed->method & false 

Contoh Template Pencarian
  phpgrep.  '$ x & $ mask == $ y'
 phpgrep.  '$ x & $ mask === $ y'
 phpgrep.  '$ x & $ mask! == $ y'
 phpgrep.  '$ x & $ mask! = $ y'
 phpgrep.  '$ x |  $ mask == $ y '
 phpgrep.  '$ x |  $ mask === $ y '
 phpgrep.  '$ x |  $ mask! == $ y '
 phpgrep.  '$ x |  $ mask! = $ y ' 

Melanjutkan topik prioritas operasi yang tidak terduga, Anda dapat membaca tentang operator ternary di PHP . Pada habr, bahkan artikel itu ditujukan untuk itu: Perintah eksekusi operator ternary .


Apakah mungkin menemukan tempat seperti itu dengan phpgrep ? Jawabannya adalah ya !


 phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_' 

Manfaat Validasi Ekspresi Reguler


βŽ† Wikia / aplikasi / pemeliharaan / wikia / updateCentralInterwiki.inc # L95 :


 if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; } 

Seperti yang dikandung oleh pembuat kode, kami memeriksa URL untuk kebetulan dengan salah satu dari 3 opsi. Simbol maaf . tidak terlindungi, yang akan mengarah pada fakta bahwa alih-alih falloutvault.com kita bisa mendapatkan falloutvaultxcom pada domain apa pun dan lulus ujian.



Ini bukan kesalahan khusus PHP. Dalam aplikasi apa pun di mana validasi dilakukan melalui ekspresi reguler dan karakter meta adalah bagian dari string yang diperiksa, ada risiko lupa melarikan diri di mana itu diperlukan dan mendapatkan kerentanan.


Anda dapat menemukan tempat-tempat seperti itu dengan menjalankan phpgrep :


 phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b' 

Kami memperkenalkan subpattern pat bernama, yang menangkap string literal apa pun, dan kemudian menerapkan filter dari ekspresi reguler untuknya.


Filter dapat diterapkan ke variabel template apa pun. Selain ekspresi reguler, ada juga operator struktural = dan != . Daftar lengkap dapat ditemukan dalam dokumentasi .


${"*"} menangkap angka sembarang argumen, jadi kami tidak perlu khawatir tentang parameter opsional fungsi preg_match .


Kunci duplikat dalam literal array


Di PHP, Anda tidak akan menerima peringatan apa pun jika Anda menjalankan kode ini:


 <?php var_dump(['a' => 1, 'a' => 2]); // : array(1) {["a"]=> int(2)} 

Kita dapat menemukan array seperti itu menggunakan phpgrep :


 [${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}] 

Pola ini dapat didekripsi sebagai berikut: "sebuah array literal di mana setidaknya ada dua kunci yang identik dalam posisi sewenang-wenang." Ekspresi ${"*"} membantu kami untuk menggambarkan "posisi sewenang-wenang", memungkinkan elemen 0-N sebelum, di antara, dan setelah kunci yang menarik bagi kami.


βŽ† Wikia / aplikasi / ekstensi / wikia / WikiaMiniUpload / WikiaMiniUpload_body.php # L23 :


 $script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), // ... ]; 

Dalam kasus ini, ini bukan kesalahan besar, tapi saya tahu kasus di mana duplikasi kunci dalam array besar (100 elemen) membawa setidaknya perilaku yang tidak terduga di mana salah satu kunci tumpang tindih dengan nilai yang lain.




Ini mengakhiri kunjungan singkat kami dengan contoh-contoh. Jika Anda menginginkan lebih, pada akhir artikel ini menjelaskan cara mendapatkan semua hasil.


Apa itu phpgrep?


Kebanyakan editor dan IDE menggunakan pencarian teks biasa untuk menemukan kode (jika itu bukan pencarian untuk karakter khusus seperti kelas atau variabel) - dengan kata lain, sesuatu seperti grep.


Anda memasukkan " $x ", cari " $x ". Ekspresi reguler mungkin tersedia untuk Anda, maka Anda sebenarnya dapat mencoba mengurai kode PHP dengan pelanggan tetap. Kadang-kadang bahkan bekerja jika Anda mencari sesuatu yang sangat spesifik dan sederhana - misalnya, "variabel apa saja dengan beberapa akhiran." Tetapi jika variabel dengan akhiran ini harus menjadi bagian dari ekspresi majemuk lain, kesulitan muncul.


phpgrep adalah alat untuk mencari kode PHP dengan mudah, yang memungkinkan Anda untuk mencari tidak menggunakan pelanggan yang berorientasi teks, tetapi menggunakan templat yang sadar sintaksis.


Sadar-sintaksis artinya bahasa templat mencerminkan bahasa target, dan tidak beroperasi pada karakter individual, seperti yang dilakukan oleh ekspresi reguler. Kami juga tidak membuat perbedaan sebelum memformat kode, hanya strukturnya yang penting.


Konten Opsional: Mulai Cepat

Mulai cepat


Instalasi


Ada build rilis siap pakai untuk amd64 untuk Linux dan Windows , tetapi jika Anda telah menginstalnya, maka satu perintah sudah cukup untuk mendapatkan biner baru untuk platform Anda:


 go get -v github.com/quasilyte/phpgrep/cmd/phpgrep 

Jika $GOPATH/bin ada dalam sistem $PATH , maka perintah phpgrep akan segera tersedia. Untuk memverifikasi ini, coba jalankan perintah dengan parameter -help :


 phpgrep -help 

Jika tidak ada yang terjadi, cari di mana Go telah menginstal biner dan menambahkannya ke variabel lingkungan $PATH .


Cara lama dan andal untuk melihat $GOPATH , meskipun tidak disetel secara eksplisit:


 go env GOPATH 

Gunakan


Buat file hello.php pengujian:


 <?php function f(...$xs) {} f(10); f(20); f(30); f($x); f(); 

Jalankan phpgrep di atasnya:


 # phpgrep hello.php 'f(${"x:int"})' 'x!=20' hello.php:3: f(10) hello.php:5: f(30) 

Kami menemukan semua panggilan ke fungsi f dengan satu argumen, nomor yang nilainya tidak sama dengan 20.


Cara kerja phpgrep


Untuk parsing PHP, pustaka github.com/z7zmey/php-parser digunakan. Ini cukup bagus, tetapi beberapa keterbatasan phpgrep mengikuti dari fitur parser yang digunakan. Terutama banyak kesulitan muncul ketika mencoba bekerja secara normal dengan kurung.


Prinsip phpgrep sederhana:


  • AST dibangun dari template input, filter dibongkar;
  • untuk setiap file input, pohon AST lengkap dibangun;
  • kita melihat-lihat AST dari setiap file, mencoba menemukan subtree yang cocok dengan polanya;
  • untuk setiap hasil daftar filter diterapkan;
  • semua hasil yang telah melewati filter dicetak ke layar.

Yang paling menarik adalah bagaimana tepatnya kedua node AST dicocokkan untuk kesetaraan. Terkadang sepele: satu-ke-satu, dan meta-node dapat menangkap lebih dari satu elemen. Contoh dari node meta adalah ${"*"} dan ${"str"} .


Kesimpulan


Akan tidak jujur ​​untuk berbicara tentang phpgrep tanpa menyebutkan pencarian struktural dan ganti (SSR) dari PhpStorm. Mereka memecahkan masalah yang sama, dan SSR memiliki kelebihan, misalnya, integrasi ke dalam IDE, dan phpgrep membanggakan bahwa itu adalah program mandiri, yang jauh lebih mudah untuk dimasukkan, misalnya, pada CI.


Antara lain, phpgrep juga merupakan perpustakaan yang dapat Anda gunakan dalam program Anda untuk mencocokkan kode PHP. Ini sangat berguna untuk linter dan pembuatan kode.


Saya akan senang jika alat ini bermanfaat bagi Anda. Jika artikel ini hanya memotivasi Anda untuk melihat ke arah RSK tersebut, juga bagus.




Bahan tambahan


Daftar lengkap pola yang digunakan untuk analisis dapat ditemukan di file patterns.txt . Di sebelah file ini, Anda dapat menemukan skrip phpgrep-lint.sh , yang menyederhanakan peluncuran phpgrep dengan daftar templat.


Artikel ini tidak memberikan daftar respons lengkap, tetapi Anda dapat mereproduksi percobaan dengan mengkloning semua repositori yang disebutkan dan menjalankan phpgrep-lint.sh di atasnya.


Anda dapat mengambil inspirasi dari templat uji, misalnya, dari artikel studio PVS . Saya sangat menyukai Ekspresi Logis: Kesalahan yang Dibuat oleh Profesional , yang berubah menjadi seperti ini:


 #  "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b' 

Anda mungkin juga tertarik dengan presentasi phpgrep: pencarian kode sadar-sintaks .


Artikel ini menggunakan gambar penjual akan yang dibuat melalui gopherkon .

Source: https://habr.com/ru/post/id464893/


All Articles