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.
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":
- Ekspresi di dalam "
if ($cond)
". - Kondisi operator ternary adalah: "
$cond ? $x : $y
". - 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? Mari kita mulai dengan (1):
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 :
Daftar templat yang diperluas untuk pemeriksaan ini 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'],
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 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]);
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 CepatMulai 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:
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 .