Kotak pasir yang ditingkatkan untuk skrip asyik


Dari seorang penerjemah: Saat mengembangkan Platform CUBA, kami menempatkan dalam kerangka kerja ini kemampuan untuk mengeksekusi skrip khusus untuk konfigurasi yang lebih fleksibel dari logika bisnis aplikasi. Apakah peluang ini baik atau buruk (dan kita berbicara tidak hanya tentang CUBA) sedang diperdebatkan untuk waktu yang lama, tetapi fakta bahwa kontrol atas eksekusi skrip pengguna diperlukan tidak menimbulkan pertanyaan. Salah satu fitur berguna Groovy untuk mengelola eksekusi skrip kustom disajikan dalam terjemahan CΓ©dric Champeau ini. Terlepas dari kenyataan bahwa ia baru-baru ini meninggalkan tim pengembangan Groovy, komunitas programmer tampaknya mengambil keuntungan dari pekerjaannya untuk waktu yang lama.


Salah satu cara yang paling umum digunakan untuk menggunakan Groovy adalah melalui scripting, karena Groovy membuatnya mudah untuk mengeksekusi kode secara dinamis dalam runtime. Tergantung pada aplikasinya, skrip dapat ditempatkan di tempat yang berbeda: sistem file, basis data, layanan jarak jauh ... tetapi yang paling penting, pengembang aplikasi yang menjalankan skrip tidak harus menulisnya. Selain itu, skrip dapat bekerja di lingkungan terbatas (memori terbatas, batas jumlah deskriptor file, runtime ...), atau Anda mungkin ingin mencegah pengguna menggunakan semua fitur bahasa dalam skrip.


Posting ini akan memberi tahu Anda.


  • mengapa asyik baik untuk menulis dsl internal
  • apa saja fitur-fiturnya dalam hal keamanan aplikasi Anda
  • cara mengkonfigurasi kompilasi untuk meningkatkan DSL
  • tentang nilai SecureASTCustomizer
  • tentang ekstensi kontrol tipe
  • cara menggunakan ekstensi kontrol tipe untuk menjadikan sandboxing efektif

Misalnya, bayangkan apa yang perlu Anda lakukan agar pengguna dapat menghitung ekspresi matematika. Salah satu opsi implementasi adalah menanamkan DSL internal, membuat parser, dan akhirnya seorang juru bahasa untuk ungkapan-ungkapan ini. Untuk melakukan ini, tentu saja, Anda harus bekerja, tetapi jika Anda perlu meningkatkan produktivitas, misalnya, dengan menghasilkan bytecode untuk ekspresi alih-alih menghitungnya dalam interpreter atau menggunakan caching kelas yang dihasilkan dalam runtime, maka Groovy adalah pilihan yang bagus.


Ada banyak opsi yang dijelaskan dalam dokumentasi , tetapi contoh paling sederhana adalah hanya menggunakan kelas Eval :


Example.java


 int sum = (Integer) Eval.me("1+1"); 

1+1 kode diuraikan, dikompilasi menjadi bytecode, dimuat dan dieksekusi oleh Groovy dalam runtime. Tentu saja, kode dalam sampel ini sangat sederhana, dan Anda perlu menambahkan parameter, tetapi idenya adalah bahwa kode yang dapat dieksekusi dapat berubah-ubah. Dan itu mungkin bukan yang Anda butuhkan. Dalam kalkulator Anda harus mengizinkan sesuatu seperti ini:


 1+1 x+y 1+(2*x)**y cos(alpha)*r v=1+x 

tapi tentu saja tidak


 println 'Hello' (0..100).each { println 'Blah' } Pong p = new Pong() println(new File('/etc/passwd').text) System.exit(-1) Eval.me('System.exit(-1)') // a script within a script! 

Di sinilah kesulitan dimulai, dan juga menjadi jelas bahwa kita perlu menyelesaikan beberapa masalah:


  • batasi tata bahasa suatu subset dari kemampuannya
  • mencegah pengguna mengeksekusi kode yang tidak disediakan
  • mencegah eksekusi kode berbahaya

Contoh dengan kalkulator ini cukup sederhana, tetapi untuk DSL yang lebih kompleks, orang mungkin tidak memperhatikan bahwa mereka sedang menulis kode bermasalah, terutama jika DSL sangat sederhana sehingga pengembang tidak dapat menggunakannya.


Beberapa tahun yang lalu saya berada dalam situasi ini. Saya mengembangkan mesin yang menjalankan "skrip" Groovy yang ditulis oleh ahli bahasa. Satu masalah, misalnya, adalah bahwa mereka secara tidak sengaja dapat membuat loop tanpa akhir. Kode dieksekusi di server, dan muncul utas melahap 100% dari CPU, setelah itu diperlukan untuk me-restart server aplikasi. Saya harus mencari cara untuk menyelesaikan masalah tanpa mempengaruhi DSL, alat atau kinerja aplikasi.


Bahkan, banyak orang memiliki kebutuhan serupa. Selama 4 tahun terakhir, saya telah berbicara dengan banyak orang yang memiliki pertanyaan yang sama: Bagaimana saya dapat mencegah pengguna melakukan omong kosong dalam skrip Groovy?


Penyusun penyesuaian


Pada saat itu, saya sudah memiliki keputusan sendiri dan saya tahu bahwa orang lain juga mengembangkan sesuatu yang serupa. Pada akhirnya, Guillaume Laforge menyarankan agar saya membuat mekanisme di kernel Groovy untuk membantu memecahkan masalah ini. Itu muncul di Groovy 1.8.0 sebagai penyesuai kompilasi .


Penyesuai kompilasi adalah sekumpulan kelas yang memodifikasi proses kompilasi skrip Groovy. Anda dapat menulis penyesuai sendiri, tetapi Groovy memasok:


  • impor penyesuai yang secara implisit menambahkan impor ke skrip sehingga pengguna tidak perlu menambahkan deskripsi impor
  • customizer AST (Abstract Syntax Tree) transformasi, memungkinkan Anda untuk menambahkan transformasi AST langsung ke skrip
  • Penyesuai AST aman membatasi konstruksi tata bahasa dan sintaksis suatu bahasa

Penyesuai transformasi AST membantu saya memecahkan masalah loop tanpa akhir dengan transformasi @ThreadInterrupt , tetapi SecureASTCustomizer adalah hal yang mungkin paling disalahpahami dalam sebagian besar kasus.


Saya harus minta maaf untuk itu. Maka saya tidak dapat menemukan nama yang lebih baik. Bagian terpenting dalam nama "SecureASTCustomizer" adalah AST . Tujuan dari mekanisme ini adalah untuk membatasi akses ke fungsi AST tertentu. Kata "aman" pada judul umumnya berlebihan, dan saya akan menjelaskan alasannya. Bahkan ada posting blog oleh Jenuke-terkenal Kosuke Kawaguchi, berjudul "Fatal Groovy SecureASTCustomizer" . Dan semuanya ditulis dengan sangat benar di sana. SecureASTCustomizer tidak dirancang untuk sandboxing. Itu dibuat untuk membatasi bahasa pada waktu kompilasi, tetapi tidak eksekusi. Sekarang saya pikir nama terbaik adalah GrammarCustomizer . Tetapi, seperti yang Anda tentu tahu, ada tiga kesulitan dalam ilmu komputer: pembatalan cache, menciptakan nama, dan kesalahan per unit.


Sekarang bayangkan Anda mempertimbangkan penyesuai AST aman sebagai cara untuk memastikan keamanan skrip Anda, dan tugas Anda adalah mencegah pengguna dari System.exit dari skrip. Dokumentasi mengatakan bahwa panggilan dapat dilarang di penerima khusus dengan membuat daftar hitam atau putih. Jika keamanan diperlukan, saya selalu merekomendasikan daftar putih yang secara ketat menyatakan apa yang diperbolehkan, tetapi bukan daftar hitam yang melarang apa pun. Karena peretas selalu memikirkan apa yang mungkin tidak Anda pertimbangkan. Saya akan memberi contoh.


Berikut cara menyiapkan mesin skrip kotak pasir primitif menggunakan SecureASTCustomizer . Meskipun saya bisa menulisnya di Groovy, saya memberikan contoh konfigurasi Java untuk membuat perbedaan antara kode integrasi dan skrip lebih eksplisit.


 public class Sandbox { public static void main(String[] args) { CompilerConfiguration conf = new CompilerConfiguration(); SecureASTCustomizer customizer = new SecureASTCustomizer(); customizer.setReceiversBlackList(Arrays.asList(System.class.getName())); conf.addCompilationCustomizers(customizer); GroovyShell shell = new GroovyShell(conf); Object v = shell.evaluate("System.exit(-1)"); System.out.println("Result = " +v); } } 

  1. buat konfigurasi kompiler
  2. buat pengubahsuaian AST aman
  3. menyatakan bahwa kelas System sebagai penerima panggilan metode dimasukkan daftar hitam
  4. tambahkan customizer ke konfigurasi kompiler
  5. mengikat konfigurasi dengan skrip shell, yaitu, mencoba membuat kotak pasir
  6. jalankan skrip "buruk"
  7. tampilkan hasil menjalankan skrip

Jika Anda menjalankan kelas ini, kesalahan akan terjadi selama eksekusi skrip:


 General error during canonicalization: Method calls not allowed on [java.lang.System] java.lang.SecurityException: Method calls not allowed on [java.lang.System] 

Kesimpulan ini dikeluarkan oleh aplikasi dengan customizer AST yang aman, yang tidak memungkinkan eksekusi metode dari kelas System . Sukses! Jadi kami telah melindungi skrip kami! Tapi tunggu sebentar ...


SecureASTCustomizer diretas!


Perlindungan, katakan? Tetapi bagaimana jika saya melakukan ini:


 def c = System c.exit(-1) 

Jika Anda menjalankan program lagi, Anda akan melihat program mogok tanpa kesalahan dan tanpa menampilkan hasilnya di layar. Kode keluar proses adalah -1, yang berarti skrip pengguna telah dijalankan! Apa yang terjadi Pada waktu kompilasi, pengubahsuaian AST aman tidak dapat mengenali bahwa c.exit pada c.exit adalah panggilan ke metode System karena ia bekerja pada level AST! Ini menganalisis pemanggilan metode, dan dalam hal ini pemanggilan metode adalah c.exit(-1) , maka ia menentukan penerima dan memeriksa apakah itu ada dalam daftar putih (atau hitam). Dalam hal ini, penerima adalah c , variabel ini dideklarasikan melalui def , dan ini sama dengan menyatakannya sebagai Object , dan pengubah AST yang aman akan berpikir bahwa jenis variabel c adalah Object , bukan System !


Secara umum, ada banyak cara untuk menyiasati berbagai konfigurasi yang dibuat pada AST customizer yang aman. Berikut ini beberapa yang keren:


 ((Object)System).exit(-1) Class.forName('java.lang.System').exit(-1) ('java.lang.System' as Class).exit(-1) import static java.lang.System.exit exit(-1) 

dan masih banyak lagi. Sifat dinamis Groovy menghalangi kemampuan untuk memperbaiki masalah ini pada waktu kompilasi. Namun, solusinya memang ada. Salah satu opsi adalah mengandalkan manajer keamanan JVM standar. Namun, ini adalah solusi kelas berat dan banyak sekali untuk seluruh sistem, dan ini setara dengan menembakkan meriam ke burung pipit. Selain itu, ini tidak berfungsi dalam semua kasus, misalnya, jika Anda ingin melarang membaca file, tetapi tidak membuat ...


Keterbatasan ini - agak disesalkan bagi banyak dari kita - menyebabkan terciptanya solusi berdasarkan cek saat runtime . Jenis cek ini tidak memiliki masalah seperti itu. Misalnya, karena Anda akan mengetahui jenis penerima pesan yang sebenarnya sebelum memulai validasi panggilan metode. Yang menarik adalah implementasi berikut:



Namun, tidak satu pun dari implementasi ini yang sepenuhnya dapat diandalkan dan aman. Sebagai contoh, versi Kosuke didasarkan pada peretasan implementasi internal situs panggilan caching. Masalahnya adalah bahwa itu tidak kompatibel dengan versi Groovy invokedynamic, dan kelas-kelas batin ini tidak akan berada di versi Groovy masa depan. Versi Simon, di sisi lain, didasarkan pada transformasi AST, tetapi meninggalkan banyak lubang potensial.


Akibatnya, teman-teman saya Corinne Crisch, Fabrice Matrat dan Sebastian Blanc, dan saya memutuskan untuk membuat mekanisme sandboxing baru dalam runtime, yang tidak akan memiliki masalah seperti proyek-proyek ini. Kami mulai menerapkannya di hackathon di Nice, dan pada konferensi Greach tahun lalu kami membuat laporan tentang hal itu . Mekanisme ini didasarkan pada transformasi AST dan pada dasarnya menulis ulang kode untuk diperiksa sebelum setiap pemanggilan metode, upaya untuk mengakses bidang kelas, menambahkan variabel, ekspresi biner ... Implementasi ini masih belum siap, dan tidak banyak pekerjaan yang telah dilakukan di atasnya, jadi ketika saya menyadari bahwa masalah dengan metode dan parameter dipanggil melalui "implisit ini" belum diselesaikan, seperti, misalnya, dalam pembangun:


 xml { cars { // cars is a method call on an implicit this: "this".cars(...) car(make:'Renault', model: 'Clio') } } 

Sampai saat ini, saya masih belum menemukan cara untuk memecahkan masalah ini karena arsitektur protokol meta-objek di Groovy, yang didasarkan pada kenyataan bahwa penerima melempar pengecualian ketika tidak dapat menemukan metode, sebelum beralih ke penerima lain. Singkatnya, ini berarti bahwa Anda tidak dapat mengetahui jenis penerima sebelum panggilan metode yang sebenarnya. Dan jika panggilan telah lewat, maka sudah terlambat ...


Dan sampai saat ini, saya tidak memiliki solusi optimal untuk masalah ini untuk kasus di mana skrip yang dapat dieksekusi menggunakan sifat dinamis dari bahasa tersebut. Tetapi sekaranglah saatnya untuk menjelaskan bagaimana Anda dapat memperbaiki situasi secara signifikan jika Anda bersedia mengorbankan sedikit kedinamisan bahasa tersebut.


Ketik memeriksa


Mari kita kembali ke masalah utama dengan SecureASTCustomizer: ini bekerja dengan pohon sintaksis abstrak dan tidak memiliki informasi tentang jenis dan penerima pesan tertentu. Tetapi dengan Groovy 2, Groovy telah menambahkan kompilasi, dan di Groovy 2.1 kami telah menambahkan ekstensi untuk pemeriksaan jenis .


Ekstensi untuk pengecekan tipe adalah hal yang sangat kuat: mereka memungkinkan pengembang DSL Groovy untuk membantu kompiler dengan inferensi tipe, dan juga memungkinkan generasi kesalahan kompilasi dalam kasus di mana mereka biasanya tidak terjadi. Ekstensi ini digunakan secara internal oleh Groovy untuk mendukung kompiler statis, misalnya, ketika menerapkan sifat atau mesin templat markup .


Bagaimana jika, alih-alih menggunakan hasil parser, kita dapat mengandalkan informasi dari mekanisme pengecekan tipe? Ambil kode yang coba ditulis oleh peretas kami:


((Object)System).exit(-1)


Jika Anda mengaktifkan pemeriksaan tipe, kode tidak dapat dikompilasi:


 1 compilation error: [Static type checking] - Cannot find matching method java.lang.Object#exit(java.lang.Integer). Please check if the declared type is right and if the method exists. 

Jadi kode ini tidak lagi dikompilasi. Dan bagaimana jika kita mengambil kode ini:


 def c = System c.exit(-1) 

Seperti yang Anda lihat, ini melewati pemeriksaan tipe, dibungkus dengan metode dan dieksekusi menggunakan perintah groovy :


 @groovy.transform.TypeChecked // or even @CompileStatic void foo() { def c = System c.exit(-1) } foo() 

Pemeriksa tipe mendeteksi bahwa metode exit dipanggil dari kelas System dan valid. Ini tidak akan membantu kami di sini. Tetapi yang kita tahu adalah bahwa jika kode ini melewati pemeriksaan tipe, itu berarti bahwa kompiler mengenali panggilan ke penerima dengan tipe System . Secara umum, idenya adalah untuk melarang panggilan dengan ekstensi untuk pemeriksaan tipe.


Ekstensi sederhana untuk pemeriksaan jenis


Sebelum mempelajari lebih lanjut tentang sandboxing, mari kita coba "mengamankan" skrip kami dengan bantuan ekstensi standar untuk pemeriksaan jenis. Mendaftarkan ekstensi semacam itu mudah: cukup atur parameter extensions untuk penjelasan @TypeChecked (atau @CompileStatic jika Anda menggunakan kompilasi statis):


 @TypeChecked(extensions=['SecureExtension1.groovy']) void foo() { def c = System c.exit(-1) } foo() 

Pencarian ekstensi akan berlangsung di classpath dalam format kode sumber (Anda dapat membuat ekstensi yang telah dikompilasi untuk pemeriksaan tipe, tetapi kami tidak akan mempertimbangkannya dalam artikel ini):


SecureExtension1.groovy


 onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } } 

  1. ketika pemeriksa tipe memilih metode untuk memanggil
  2. jika metode milik System kelas
  3. kemudian biarkan pemeriksa ketik menghasilkan kesalahan

Itu saja yang Anda butuhkan. Sekarang jalankan kode lagi dan Anda akan melihat kesalahan kompilasi!


 /home/cchampeau/tmp/securetest.groovy: 6: [Static type checking] - Method call is not allowed! @ line 6, column 3. c.exit(-1) ^ 1 error 

Kali ini, berkat type checker, c diakui sebagai turunan dari kelas System , dan kami dapat melarang panggilan. Ini adalah contoh yang sangat sederhana, dan tidak menunjukkan semua yang dapat dilakukan dengan penyesuai AST aman dalam hal konfigurasi. Dalam ekstensi yang kami tulis , cek di- hardcode , tetapi mungkin lebih baik membuatnya disesuaikan. Jadi mari kita buat contoh lebih rumit.


Misalkan aplikasi Anda menghitung metrik tertentu untuk dokumen dan memungkinkan pengguna untuk menyesuaikannya. Dalam hal ini, DSL:


  • akan mengoperasikan (setidaknya) variabel score
  • memungkinkan pengguna untuk melakukan operasi matematika (termasuk memanggil metode cos , abs , ...)
  • harus melarang semua metode lain

Contoh skrip pengguna:


abs(cos(1+score))


DSL ini mudah dikonfigurasi. Ini adalah varian dari apa yang kami definisikan di atas:


Sandbox.java


 CompilerConfiguration conf = new CompilerConfiguration(); ImportCustomizer customizer = new ImportCustomizer(); customizer.addStaticStars("java.lang.Math"); conf.addCompilationCustomizers(customizer); Binding binding = new Binding(); binding.setVariable("score", 2.0d); GroovyShell shell = new GroovyShell(binding,conf); Double userScore = (Double) shell.evaluate("abs(cos(1+score))"); System.out.println("userScore = " + userScore); 

  1. tambahkan importizer yang akan menambahkan import static java.lang.Math.* ke semua skrip
  2. buat variabel score tersedia untuk skrip
  3. jalankan skrip

Ada beberapa cara untuk men-cache skrip alih-alih menguraikan dan mengompilasinya setiap kali. Lihat dokumentasi untuk detailnya.


Jadi, skrip kami berfungsi, tetapi tidak ada yang mencegah peretas meluncurkan kode berbahaya. Karena kami berencana untuk menggunakan pengecekan tipe, saya akan merekomendasikan menggunakan transformasi @CompileStatic :


  • itu mengaktifkan pemeriksaan jenis dalam skrip, dan kami akan dapat melakukan pemeriksaan tambahan berkat ekstensi untuk pemeriksaan jenis
  • meningkatkan kinerja skrip

Menambahkan anotasi @CompileStatic ke skrip Anda secara tersirat cukup sederhana. Anda hanya perlu memperbarui konfigurasi kompiler:


 ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(CompileStatic.class); conf.addCompilationCustomizers(astcz); 

Sekarang jika Anda mencoba menjalankan skrip lagi, Anda akan melihat kesalahan kompilasi:


 Script1.groovy: 1: [Static type checking] - The variable [score] is undeclared. @ line 1, column 11. abs(cos(1+score)) ^ Script1.groovy: 1: [Static type checking] - Cannot find matching method int#plus(java.lang.Object). Please check if the declared type is right and if the method exists. @ line 1, column 9. abs(cos(1+score)) ^ 2 errors 

Apa yang terjadi Jika Anda membaca skrip dari sudut pandang kompiler, menjadi jelas bahwa dia tidak tahu apa-apa tentang variabel "skor". Tetapi Anda, sebagai pengembang, tahu bahwa ini adalah variabel double , tetapi kompiler tidak dapat menampilkannya. Untuk ini, ekstensi untuk pemeriksaan jenis dibuat: Anda dapat memberikan informasi tambahan kepada kompiler, dan kompilasi akan berfungsi dengan baik. Dalam hal ini, kita perlu menunjukkan bahwa variabel score bertipe double .


Oleh karena itu, Anda dapat sedikit mengubah cara anotasi @CompileStatic :


 ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension2.groovy")), CompileStatic.class); 

Ini "mengemulasi" kode yang dianotasi oleh @CompileStatic(extensions=['SecureExtension2.groovy']) . Sekarang, tentu saja, kita perlu menulis ekstensi yang akan mengenali variabel score :


SecureExtension2.groovy


 unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } } 

  1. dalam pemeriksa tipe kasus tidak dapat menentukan variabel
  2. jika nama variabel adalah score
  3. biarkan kompiler mendefinisikan variabel secara dinamis dengan tipe double

Deskripsi lengkap ekstensi DSL untuk pemeriksaan jenis dapat ditemukan di bagian dokumentasi ini , tetapi ada contoh mode kompilasi gabungan: kompilator tidak dapat menentukan variabel score . Anda, sebagai pengembang DSL, tahu bahwa variabel itu sebenarnya jenisnya - double , jadi panggilan untuk membuat makeDynamic sini untuk mengatakan: "ok, jangan khawatir, saya tahu apa yang saya lakukan, variabel ini dapat didefinisikan secara dinamis dengan jenis double " Itu saja!


Ekstensi "aman" pertama selesai


Sekarang mari kita kumpulkan semuanya. Kami menulis satu ekstensi pengecekan tipe yang mencegah panggilan ke metode kelas System di satu sisi dan yang lain yang mendefinisikan variabel score di sisi lain. Jadi, jika kita menghubungkannya, kita mendapatkan ekstensi penuh pertama untuk pemeriksaan tipe:


SecureExtension3.groovy


 // disallow calls on System onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } } // resolve the score variable unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } } 

Ingatlah untuk memperbarui konfigurasi di kelas Java Anda untuk menggunakan ekstensi baru untuk pemeriksaan tipe:


 ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension3.groovy")), CompileStatic.class); 

Jalankan kode lagi - masih berfungsi. Sekarang coba ini:


 abs(cos(1+score)) System.exit(-1) 

Kompilasi skrip akan macet dengan kesalahan:


 Script1.groovy: 1: [Static type checking] - Method call is not allowed! @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error 

Selamat, Anda baru saja menulis ekstensi pengecekan tipe pertama yang mencegah kode berbahaya berjalan!


Konfigurasi ekstensi yang ditingkatkan


Jadi, semuanya berjalan dengan baik, kita dapat melarang panggilan ke metode kelas System , tetapi tampaknya kerentanan baru akan segera ditemukan, dan kita perlu mencegah peluncuran kode berbahaya. Jadi, alih-alih hardcode semua yang ada di ekstensi, kami akan mencoba membuat ekstensi kami universal dan dapat disesuaikan. Ini mungkin yang paling sulit, karena tidak ada cara langsung untuk meneruskan konteks ke ekstensi untuk pemeriksaan tipe. Gagasannya, oleh karena itu, didasarkan pada penggunaan variabel lokal thread (metode kurva, ya) untuk meneruskan data konfigurasi ke tipe checker.


Pertama-tama, kita akan membuat daftar variabel yang dapat disesuaikan. Ini akan menjadi seperti apa kode Java:


Sandbox.java


 public class Sandbox { public static final String VAR_TYPES = "sandboxing.variable.types"; public static final ThreadLocal<Map<String, Object>> COMPILE_OPTIONS = new ThreadLocal<>(); public static void main(String[] args) { CompilerConfiguration conf = new CompilerConfiguration(); ImportCustomizer customizer = new ImportCustomizer(); customizer.addStaticStars("java.lang.Math"); ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension4.groovy")), CompileStatic.class); conf.addCompilationCustomizers(astcz); conf.addCompilationCustomizers(customizer); Binding binding = new Binding(); binding.setVariable("score", 2.0d); try { Map<String,ClassNode> variableTypes = new HashMap<String, ClassNode>(); variableTypes.put("score", ClassHelper.double_TYPE); Map<String,Object> options = new HashMap<String, Object>(); options.put(VAR_TYPES, variableTypes); COMPILE_OPTIONS.set(options); GroovyShell shell = new GroovyShell(binding, conf); Double userScore = (Double) shell.evaluate("abs(cos(1+score));System.exit(-1)"); System.out.println("userScore = " + userScore); } finally { COMPILE_OPTIONS.remove(); } } } 

  1. ThreadLocal ,
  2. β€” SecureExtension4.groovy
  3. variableTypes β€” β€œ β†’ ”
  4. score
  5. options β€”
  6. "variable types" VAR_TYPES
  7. thread local
  8. , , thread local

:


 import static Sandbox.* def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] unresolvedVariable { var -> if (typesOfVariables[var.name]) { return makeDynamic(var, typesOfVariables[var.name]) } } 

  1. thread local
  2. , ,
  3. type checker

thread local, , type checker . , unresolvedVariable , , , type checker, . , . !


. , .



. , . , , . , System.exit , :


 java.lang.System#exit(int) 

, Java, :


 public class Sandbox { public static final String WHITELIST_PATTERNS = "sandboxing.whitelist.patterns"; // ... public static void main(String[] args) { // ... try { Map<String,ClassNode> variableTypes = new HashMap<String, ClassNode>(); variableTypes.put("score", ClassHelper.double_TYPE); Map<String,Object> options = new HashMap<String, Object>(); List<String> patterns = new ArrayList<String>(); patterns.add("java\\.lang\\.Math#"); options.put(VAR_TYPES, variableTypes); options.put(WHITELIST_PATTERNS, patterns); COMPILE_OPTIONS.set(options); GroovyShell shell = new GroovyShell(binding, conf); Double userScore = (Double) shell.evaluate("abs(cos(1+score));System.exit(-1)"); System.out.println("userScore = " + userScore); } finally { COMPILE_OPTIONS.remove(); } } } 

  1. java.lang.Math

:


 import groovy.transform.CompileStatic import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.MethodNode import org.codehaus.groovy.ast.Parameter import org.codehaus.groovy.transform.stc.ExtensionMethodNode import static Sandbox.* @CompileStatic private static String prettyPrint(ClassNode node) { node.isArray()?"${prettyPrint(node.componentType)}[]":node.toString(false) } @CompileStatic private static String toMethodDescriptor(MethodNode node) { if (node instanceof ExtensionMethodNode) { return toMethodDescriptor(node.extensionMethodNode) } def sb = new StringBuilder() sb.append(node.declaringClass.toString(false)) sb.append("#") sb.append(node.name) sb.append('(') sb.append(node.parameters.collect { Parameter it -> prettyPrint(it.originType) }.join(',')) sb.append(')') sb } def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] def whiteList = COMPILE_OPTIONS.get()[WHITELIST_PATTERNS] onMethodSelection { expr, MethodNode methodNode -> def descr = toMethodDescriptor(methodNode) if (!whiteList.any { descr =~ it }) { addStaticTypeError("You tried to call a method which is not allowed, what did you expect?: $descr", expr) } } unresolvedVariable { var -> if (typesOfVariables[var.name]) { return makeDynamic(var, typesOfVariables[var.name]) } } 

  1. MethodNode
  2. thread local
  3. ,

, :


 Script1.groovy: 1: [Static type checking] - You tried to call a method which is not allowed, what did you expect?: java.lang.System#exit(int) @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error 

, ! , , . , ! , , . , ( foo.text , foo.getText() ).



, type checker' "property selection", , . , , . , , β€” . .


SandboxingTypeCheckingExtension.groovy


 import groovy.transform.CompileStatic import org.codehaus.groovy.ast.ClassCodeVisitorSupport import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.MethodNode import org.codehaus.groovy.ast.Parameter import org.codehaus.groovy.ast.expr.PropertyExpression import org.codehaus.groovy.control.SourceUnit import org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys import org.codehaus.groovy.transform.stc.ExtensionMethodNode import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport import static Sandbox.* class SandboxingTypeCheckingExtension extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL { @CompileStatic private static String prettyPrint(ClassNode node) { node.isArray()?"${prettyPrint(node.componentType)}[]":node.toString(false) } @CompileStatic private static String toMethodDescriptor(MethodNode node) { if (node instanceof ExtensionMethodNode) { return toMethodDescriptor(node.extensionMethodNode) } def sb = new StringBuilder() sb.append(node.declaringClass.toString(false)) sb.append("#") sb.append(node.name) sb.append('(') sb.append(node.parameters.collect { Parameter it -> prettyPrint(it.originType) }.join(',')) sb.append(')') sb } @Override Object run() { // Fetch white list of regular expressions of authorized method calls def whiteList = COMPILE_OPTIONS.get()[WHITELIST_PATTERNS] def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] onMethodSelection { expr, MethodNode methodNode -> def descr = toMethodDescriptor(methodNode) if (!whiteList.any { descr =~ it }) { addStaticTypeError("You tried to call a method which is not allowed, what did you expect?: $descr", expr) } } unresolvedVariable { var -> if (isDynamic(var) && typesOfVariables[var.name]) { storeType(var, typesOfVariables[var.name]) handled = true } } // handling properties (like foo.text) is harder because the type checking extension // does not provide a specific hook for this. Harder, but not impossible! afterVisitMethod { methodNode -> def visitor = new PropertyExpressionChecker(context.source, whiteList) visitor.visitMethod(methodNode) } } private class PropertyExpressionChecker extends ClassCodeVisitorSupport { private final SourceUnit unit private final List<String> whiteList PropertyExpressionChecker(final SourceUnit unit, final List<String> whiteList) { this.unit = unit this.whiteList = whiteList } @Override protected SourceUnit getSourceUnit() { unit } @Override void visitPropertyExpression(final PropertyExpression expression) { super.visitPropertyExpression(expression) ClassNode owner = expression.objectExpression.getNodeMetaData(StaticCompilationMetadataKeys.PROPERTY_OWNER) if (owner) { if (expression.spreadSafe && StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf(owner, classNodeFor(Collection))) { owner = typeCheckingVisitor.inferComponentType(owner, ClassHelper.int_TYPE) } def descr = "${prettyPrint(owner)}#${expression.propertyAsString}" if (!whiteList.any { descr =~ it }) { addStaticTypeError("Property is not allowed: $descr", expression) } } } } }```     sandbox',     assert' ,  ,     : ``Sandbox.java`` ```java public class Sandbox { public static final String WHITELIST_PATTERNS = "sandboxing.whitelist.patterns"; public static final String VAR_TYPES = "sandboxing.variable.types"; public static final ThreadLocal<Map<String, Object>> COMPILE_OPTIONS = new ThreadLocal<Map<String, Object>>(); public static void main(String[] args) { CompilerConfiguration conf = new CompilerConfiguration(); ImportCustomizer customizer = new ImportCustomizer(); customizer.addStaticStars("java.lang.Math"); ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SandboxingTypeCheckingExtension.groovy")), CompileStatic.class); conf.addCompilationCustomizers(astcz); conf.addCompilationCustomizers(customizer); Binding binding = new Binding(); binding.setVariable("score", 2.0d); try { Map<String, ClassNode> variableTypes = new HashMap<String, ClassNode>(); variableTypes.put("score", ClassHelper.double_TYPE); Map<String, Object> options = new HashMap<String, Object>(); List<String> patterns = new ArrayList<String>(); // allow method calls on Math patterns.add("java\\.lang\\.Math#"); // allow constructors calls on File patterns.add("File#<init>"); // because we let the user call each/times/... patterns.add("org\\.codehaus\\.groovy\\.runtime\\.DefaultGroovyMethods"); options.put(VAR_TYPES, variableTypes); options.put(WHITELIST_PATTERNS, patterns); COMPILE_OPTIONS.set(options); GroovyShell shell = new GroovyShell(binding, conf); Object result; try { result = shell.evaluate("Eval.me('1')"); // error assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("System.exit(-1)"); // error assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("((Object)Eval).me('1')"); // error assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("new File('/etc/passwd').getText()"); // getText is not allowed assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } try { result = shell.evaluate("new File('/etc/passwd').text"); // getText is not allowed assert false; } catch (MultipleCompilationErrorsException e) { System.out.println("Successful sandboxing: "+e.getMessage()); } Double userScore = (Double) shell.evaluate("abs(cos(1+score))"); System.out.println("userScore = " + userScore); } finally { COMPILE_OPTIONS.remove(); } } } 

Kesimpulan


Groovy JVM. , . , , , . , Groovy, sandboxing' (, , ).


, , . , . , , .


, sandboxing', , β€” SecureASTCustomizer . , , : secure AST customizer , (, ), ( , ).


, : , , . Groovy . Groovy, , - pull request, - !

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


All Articles