
من المترجم: عند تطوير منصة CUBA ، نضع في هذا الإطار القدرة على تنفيذ البرامج النصية المخصصة من أجل تكوين أكثر مرونة لمنطق أعمال التطبيق. سواء كانت هذه الفرصة جيدة أو سيئة (ونحن نتحدث ليس فقط عن CUBA) قيد المناقشة لفترة طويلة ، ولكن حقيقة أن السيطرة على تنفيذ البرامج النصية للمستخدم ضرورية لا تثير أي أسئلة. يتم تقديم إحدى ميزات Groovy المفيدة لإدارة تنفيذ البرنامج النصي المخصص في هذه الترجمة من Cédric Champeau. على الرغم من أنه غادر فريق تطوير Groovy مؤخرًا ، يبدو أن مجتمع المبرمجين يستفيد من جهوده لفترة طويلة قادمة.
تعد البرمجة النصية واحدة من أكثر الطرق شيوعًا لاستخدام Groovy ، حيث يجعل تطبيق Groovy من السهل تنفيذ التعليمات البرمجية ديناميكيًا في وقت التشغيل. اعتمادًا على التطبيق ، يمكن وضع البرامج النصية في أماكن مختلفة: نظام الملفات وقاعدة البيانات والخدمات عن بعد ... ولكن الأهم من ذلك أن مطور التطبيق الذي ينفذ البرامج النصية لا يكتبها بالضرورة. علاوة على ذلك ، يمكن أن تعمل البرامج النصية في بيئة محدودة (ذاكرة محدودة ، تحد من عدد واصفات الملفات ، وقت التشغيل ...) ، أو قد ترغب في منع المستخدم من استخدام جميع ميزات اللغة في البرنامج النصي.
هذا المنصب سوف اقول لك.
- لماذا رائع رائع لكتابة dsl داخلي
- ما هي ميزاته من حيث أمان التطبيق الخاص بك
- كيفية تكوين تجميع لتحسين DSL
- حول قيمة
SecureASTCustomizer
- حول ملحقات التحكم نوع
- كيفية استخدام امتدادات التحكم في الكتابة لجعل الصندوق فعال
على سبيل المثال ، تخيل ما عليك القيام به حتى يتمكن المستخدم من حساب التعبيرات الرياضية. يتمثل أحد خيارات التطبيق في تضمين DSL داخلي ، وإنشاء محلل ، وأخيرا مترجم لهذه التعبيرات. للقيام بذلك ، بالطبع ، سيكون عليك العمل ، ولكن إذا كنت بحاجة إلى زيادة الإنتاجية ، على سبيل المثال ، عن طريق إنشاء رمز ثانوي للتعبيرات بدلاً من حسابها في المترجم الشفهي أو استخدام التخزين المؤقت للفئات التي تم إنشاؤها في وقت التشغيل ، فإن Groovy يعد خيارًا رائعًا.
هناك العديد من الخيارات الموضحة في الوثائق ، ولكن المثال الأبسط هو مجرد استخدام فئة Eval
:
Example.java
int sum = (Integer) Eval.me("1+1");
يتم تحليل الكود 1+1
، وتجميعه في الكود الثنائي ، وتحميله وتنفيذه بواسطة Groovy في وقت التشغيل. بالطبع ، الكود الموجود في هذه العينة بسيط للغاية ، وستحتاج إلى إضافة معلمات ، لكن الفكرة أن التعليمات البرمجية القابلة للتنفيذ يمكن أن تكون عشوائية. وهذا قد لا يكون بالضبط ما تحتاجه. في الآلة الحاسبة تحتاج إلى السماح بشيء من هذا القبيل:
1+1 x+y 1+(2*x)**y cos(alpha)*r v=1+x
لكن بالتأكيد لا
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!
وهنا تبدأ الصعوبات ، ويتضح أيضًا أننا بحاجة إلى حل العديد من المشكلات:
- حصر قواعد اللغة في مجموعة فرعية من قدراتها
- منع المستخدمين من تنفيذ التعليمات البرمجية
- منع تنفيذ التعليمات البرمجية الضارة
المثال مع الآلة الحاسبة بسيط للغاية ، لكن بالنسبة إلى DSL الأكثر تعقيدًا ، قد لا يلاحظ الأشخاص أنهم يكتبون رمزًا إشكاليًا ، خاصةً إذا كانت DSL بسيطة جدًا بحيث لا يستطيع المطورون استخدامها.
قبل بضع سنوات كنت في هذه الحالة. لقد قمت بتطوير محرك يقوم بتشغيل "نصوص" رائعة لـ Groovy كتبها علماء اللغة. مشكلة واحدة ، على سبيل المثال ، هي أنها يمكن أن تخلق عن غير قصد حلقة لا نهاية لها. تم تنفيذ الكود على الخادم ، وظهر مؤشر ترابط يلتهم 100٪ من وحدة المعالجة المركزية ، وبعد ذلك كان من الضروري إعادة تشغيل خادم التطبيق. اضطررت للبحث عن طريقة لحل المشكلة دون التأثير على DSL أو الأدوات أو أداء التطبيق.
في الواقع ، كثير من الناس لديهم احتياجات مماثلة. خلال السنوات الأربع الماضية ، كنت أتحدث مع الكثير من الأشخاص الذين لديهم نفس السؤال: كيف يمكنني منع المستخدمين من القيام بهراء في البرامج النصية لـ Groovy؟
المجمعين التخصيص
في ذلك الوقت ، كان لدي بالفعل قراري الخاص وكنت أعرف أن الآخرين قد طوروا شيئًا مشابهًا. في النهاية ، اقترح Guillaume Laforge أن أقوم بإنشاء آلية في نواة Groovy للمساعدة في حل هذه المشكلات. ظهر في Groovy 1.8.0 كمخصصات ترجمة .
أدوات تخصيص التجميع هي مجموعة من الفئات التي تقوم بتعديل عملية التحويل البرمجي للبرامج النصية لـ Groovy. يمكنك كتابة المخصص الخاص بك ، ولكن لوازم Groovy:
- استيراد أداة تخصيص تضيف الواردات إلى البرامج النصية ضمنيًا حتى لا يحتاج المستخدمون إلى إضافة أوصاف الاستيراد
- تحويلات AST (شجرة سياق الملخص) ، مما يتيح لك إضافة تحويلات AST مباشرة إلى البرامج النصية
- تأمين مخصص AST تقييد بنيات قواعد اللغة وبناء الجملة للغة
ساعدني مُخصصات تحويلات AST في حل مشكلة الحلقة التي لا @ThreadInterrupt
مع تحول @ThreadInterrupt
، لكن SecureASTCustomizer هو الشيء الذي يُساء فهمه على الأرجح في الغالبية العظمى من الحالات.
يجب أن أعتذر عن ذلك. ثم لم أستطع التوصل إلى اسم أفضل. الجزء الأكثر أهمية في اسم "SecureASTCustomizer" هو AST . كان الغرض من هذه الآلية هو الحد من الوصول إلى بعض وظائف AST. كلمة "آمنة" في العنوان غير ضرورية بشكل عام ، وسأشرح لك السبب. هناك حتى مدونة لكوكوك كاواجوتشي الشهيرة في جينكينز ، بعنوان "Fatal Groovy SecureASTCustomizer" . وكل شيء مكتوب بشكل صحيح جدا هناك. لم يتم تصميم SecureASTCustomizer من أجل صندوق الحماية. تم إنشاؤه للحد من اللغة في وقت الترجمة ، ولكن ليس التنفيذ. الآن أعتقد أن أفضل اسم سيكون GrammarCustomizer . ولكن ، كما تعلمون بالتأكيد ، هناك ثلاث صعوبات في علوم الكمبيوتر: إبطال ذاكرة التخزين المؤقت ، واختراع الأسماء ، وخطأ لكل وحدة.
الآن تخيل أنك تفكر في تخصيص AST الآمن كوسيلة لضمان أمان البرنامج النصي ، ومهمتك هي منع المستخدم من System.exit
من البرنامج النصي. تشير الوثائق إلى أنه يمكن حظر المكالمات في أجهزة الاستقبال الخاصة عن طريق إنشاء قوائم سوداء أو بيضاء. إذا كانت هناك حاجة للأمان ، فإنني أوصي دائمًا بالقوائم البيضاء التي تحدد بدقة ما هو مسموح به ، ولكن ليس قوائم سوداء تحظر أي شيء. لأن المتسللين يفكرون دائمًا في ما قد لا تفكر فيه. سأقدم مثالا.
فيما يلي كيفية إعداد مشغل نصوص رمل بدائي باستخدام SecureASTCustomizer
. على الرغم من أنني أستطيع كتابتها في Groovy ، إلا أنني أعطيت أمثلة لتكوين Java لجعل الفرق بين رمز التكامل والبرامج النصية أكثر وضوحًا.
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); } }
- إنشاء تكوين المترجم
- إنشاء آمنة مخصصة AST
- الإعلان عن إدراج فئة
System
كمستقبل لمكالمات الطريقة - إضافة مخصص لتكوين المترجم
- ربط التكوين مع البرنامج النصي shell ، أي محاولة إنشاء رمل
- قم بتشغيل البرنامج النصي "السيئ"
- عرض نتيجة تشغيل البرنامج النصي
إذا قمت بتشغيل هذه الفئة ، فسيحدث خطأ أثناء تنفيذ البرنامج النصي:
General error during canonicalization: Method calls not allowed on [java.lang.System] java.lang.SecurityException: Method calls not allowed on [java.lang.System]
يتم إصدار هذا الاستنتاج بواسطة تطبيق مزود بمخصص AST آمن ، والذي لا يسمح بتنفيذ أساليب فئة System
. النجاح! لذلك قمنا بحماية السيناريو لدينا! لكن انتظر لحظة ...
تم اختراق SecureASTCustomizer!
حماية ، ويقول؟ ولكن ماذا لو فعلت هذا:
def c = System c.exit(-1)
إذا قمت بتشغيل البرنامج مرة أخرى ، فسترى أنه يتعطل بدون خطأ ودون عرض النتيجة على الشاشة. رمز خروج العملية هو -1 ، مما يعني أنه تم تشغيل البرنامج النصي للمستخدم! ماذا حدث في وقت الترجمة ، يتعذر على أداة تخصيص AST الآمنة إدراك أن c.exit
هي استدعاء لطريقة System
حيث المبدأ لأنها تعمل على مستوى AST! يحلل استدعاء الأسلوب ، وفي هذه الحالة ، استدعاء الأسلوب c.exit(-1)
، ثم يحدد جهاز الاستقبال ويتحقق مما إذا كان c.exit(-1)
في القائمة البيضاء (أو السوداء). في هذه الحالة ، يكون المتلقي c
، ويتم الإعلان عن هذا المتغير عبر def ، وهذا هو نفسه التصريح عنه Object
، Object
أداة تخصيص AST الآمنة أن نوع المتغير c
هو Object
، وليس System
!
بشكل عام ، هناك العديد من الطرق للتغلب على التكوينات المختلفة التي تم إنشاؤها على مخصص AST الآمن. إليكم بعض الأشياء الرائعة:
((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)
ويمكن أن يكون هناك الكثير . الطبيعة الديناميكية لـ Groovy تحول دون القدرة على حل هذه المشكلات في وقت الترجمة. ومع ذلك ، يوجد حل. أحد الخيارات هو الاعتماد على مدير أمان JVM القياسي. ومع ذلك ، هذا هو الحل الثقيل والوزن الضخم على الفور للنظام بأكمله ، وهذا يعادل إطلاق مدفع في العصافير. بالإضافة إلى ذلك ، لا يعمل في جميع الحالات ، على سبيل المثال ، إذا كنت ترغب في حظر قراءة الملفات ، ولكن لا تنشئ ...
هذا القيد - بالأحرى كئيبة للكثيرين منا - أدى إلى إيجاد حل يعتمد على الشيكات في وقت التشغيل . هذا النوع من الفحص ليس لديه مثل هذه المشاكل. على سبيل المثال ، لأنك ستعرف نوع المستقبل الفعلي للرسالة قبل البدء في التحقق من صحة استدعاء الأسلوب. ذات أهمية خاصة هي التطبيقات التالية:
ومع ذلك ، فإن أيا من هذه التطبيقات موثوقة وآمنة تماما. على سبيل المثال ، يعتمد إصدار Kosuke على اختراق التطبيق الداخلي لموقع استدعاء التخزين المؤقت. المشكلة هي أنه غير متوافق مع إصدار invokedynamic من Groovy ، ولن تكون هذه الفئات الداخلية في الإصدارات المستقبلية من Groovy. نسخة سيمون ، من ناحية أخرى ، تعتمد على تحويلات AST ، ولكنها تترك العديد من الثقوب المحتملة.
ونتيجة لذلك ، قررت أنا وأصدقائي Corinne Crisch و Fabrice Matrat و Sebastian Blanc ، إنشاء آلية جديدة لصناديق الرمل في وقت التشغيل ، والتي لن تواجه مشكلات مثل هذه المشاريع. بدأنا في تنفيذه في هاكاثون في نيس ، وفي مؤتمر غريتش في العام الماضي قدمنا تقريراً عنه . تعتمد هذه الآلية على تحويلات AST وتعيد كتابة الكود بشكل أساسي للتحقق قبل كل استدعاء للطريقة ، ومحاولة الوصول إلى حقل الفصل ، وزيادة متغير ، تعبير ثنائي ... هذا التطبيق لا يزال غير جاهز ، ولم يتم إنجاز الكثير من العمل عليه ، لذلك كما أدركت أن مشكلة الطرق والمعلمات التي تم استدعاؤها من خلال "ضمني هذا" لم يتم حلها بعد ، على سبيل المثال ، في البناة:
xml { cars { // cars is a method call on an implicit this: "this".cars(...) car(make:'Renault', model: 'Clio') } }
حتى الآن ، ما زلت لم أجد طريقة لحل هذه المشكلة بسبب بنية بروتوكول كائن التعريف في Groovy ، والتي تستند إلى حقيقة أن المتلقي يلقي استثناءً عندما يتعذر عليه العثور على الطريقة ، قبل التبديل إلى مستقبل آخر. باختصار ، هذا يعني أنه لا يمكنك معرفة نوع جهاز الاستقبال قبل استدعاء الأسلوب الفعلي. وإذا مرت المكالمة ، فوات الأوان ...
وحتى وقت قريب ، لم يكن لدي حل مثالي لهذه المشكلة للحالة التي يستخدم فيها البرنامج النصي القابل للتنفيذ الخصائص الديناميكية للغة. ولكن الآن حان الوقت لشرح كيف يمكنك تحسين الموقف بشكل كبير إذا كنت مستعدًا للتضحية قليلاً من ديناميكية اللغة.
اكتب التحقق
دعنا نعود إلى المشكلة الرئيسية في SecureASTCustomizer: إنه يعمل مع شجرة بناء جملة مجردة وليس لديه معلومات حول أنواع الرسائل وأجهزة الاستقبال المحددة. ولكن مع Groovy 2 ، أضاف Groovy تصنيفًا ، وفي Groovy 2.1 أضفنا ملحقات لفحص النوع .
تعد الإمتدادات لفحص النوع أمرًا قويًا للغاية: فهي تسمح لمطور Groovy DSL بمساعدة برنامج التحويل البرمجي باستنتاج الكتابة ، كما يسمح أيضًا بإنشاء أخطاء التحويل البرمجي في الحالات التي لا تحدث فيها عادةً. يتم استخدام هذه الملحقات داخليًا بواسطة Groovy لدعم برنامج التحويل البرمجي الثابت ، على سبيل المثال ، عند تنفيذ السمات أو محرك قالب العلامات .
ماذا لو بدلاً من استخدام نتائج المحلل اللغوي ، يمكننا الاعتماد على معلومات من آلية التحقق من النوع؟ خذ الكود الذي حاول أحد المتسللين كتابته:
((Object)System).exit(-1)
إذا قمت بتنشيط اختبارات الكتابة ، فلن يتم تجميع الشفرة:
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.
لذلك هذا الرمز لم يعد يجمع. وماذا لو أخذنا هذا الكود:
def c = System c.exit(-1)
كما ترون ، يجتاز اختبار النوع ، ملفوفًا بطريقة ويتم تنفيذه باستخدام الأمر groovy
:
@groovy.transform.TypeChecked
يكشف مدقق النوع أن طريقة exit
تسمى من فئة System
وأنها صالحة. هذا لن يساعدنا هنا. لكن ما نعرفه هو أنه إذا نجح هذا الرمز في التحقق من النوع ، فهذا يعني أن المترجم يتعرف على المكالمة إلى المتلقي بالنوع System
. بشكل عام ، تتمثل الفكرة في حظر مكالمة ذات امتداد لفحص النوع.
تمديد بسيط لفحص النوع
قبل الخوض في وضع الحماية في التفاصيل ، دعونا نحاول "تأمين" نصنا بمساعدة ملحق قياسي لفحص الكتابة. يسهل تسجيل هذا الامتداد: فقط قم بتعيين المعلمة extensions
@TypeChecked
التوضيحي @TypeChecked
(أو @CompileStatic
إذا كنت تستخدم @CompileStatic
البرمجي الثابت):
@TypeChecked(extensions=['SecureExtension1.groovy']) void foo() { def c = System c.exit(-1) } foo()
ستتم عملية البحث عن الإضافات في classpath بتنسيق التعليمات البرمجية المصدر (يمكنك إنشاء ملحقات تم تجميعها مسبقًا للتحقق من النوع ، لكننا لن نأخذها في الاعتبار في هذه المقالة):
SecureExtension1.groovy
onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } }
- عندما يختار المدقق طريقة للاتصال
- إذا كانت الطريقة تنتمي إلى
System
الفصل - ثم اسمح للمدقق بإنشاء خطأ
هذا كل ما تحتاجه. الآن قم بتشغيل الكود مرة أخرى وسترى خطأً في الترجمة!
/home/cchampeau/tmp/securetest.groovy: 6: [Static type checking] - Method call is not allowed! @ line 6, column 3. c.exit(-1) ^ 1 error
هذه المرة ، وبفضل type checker ، c
التعرف على c
كمثيل لفئة System
، ويمكننا منع المكالمة. هذا مثال بسيط للغاية ، ولا يوضح كل ما يمكن القيام به باستخدام أداة تخصيص AST الآمنة من حيث التكوين. في الإضافة التي كتبناها ، تكون الشيكات مضمنة ، ولكن قد يكون من الأفضل جعلها قابلة للتخصيص. لذلك دعونا نجعل المثال أكثر تعقيدًا.
افترض أن تطبيقك يحسب مقاييس معينة للمستند ويسمح للمستخدمين بتخصيصها. في هذه الحالة ، DSL:
- سوف تعمل (على الأقل) متغير
score
- يسمح للمستخدمين بإجراء عمليات رياضية (بما في ذلك استدعاء أساليب cos و abs و ...)
- يجب أن تمنع جميع الأساليب الأخرى
نموذج البرنامج النصي للمستخدم:
abs(cos(1+score))
هذا DSL سهل التكوين. هذا هو البديل لما حددناه أعلاه:
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);
- إضافة أداة تخصيص الاستيراد التي ستضيف
import static java.lang.Math.*
إلى جميع البرامج النصية - جعل متغير
score
المتاحة للبرنامج النصي - تنفيذ البرنامج النصي
هناك طرق لتخزين البرامج النصية مؤقتًا بدلاً من تحليلها وتجميعها في كل مرة. انظر الوثائق للحصول على التفاصيل.
لذلك ، يعمل البرنامج النصي الخاص بنا ، ولكن لا شيء يمنع المتسلل من إطلاق تعليمات برمجية ضارة. نظرًا لأننا نخطط لاستخدام التحقق من النوع ، أوصي باستخدام تحويل @CompileStatic
:
- إنه ينشط عملية تدقيق الكتابة في البرنامج النصي ، وسنكون قادرين على إجراء عمليات فحص إضافية بفضل الامتداد لفحص الكتابة
- تحسين أداء البرنامج النصي
من السهل جدًا إضافة التعليقات التوضيحية @CompileStatic
إلى البرامج النصية الخاصة بك. تحتاج فقط إلى تحديث تكوين برنامج التحويل البرمجي:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(CompileStatic.class); conf.addCompilationCustomizers(astcz);
الآن إذا حاولت تشغيل البرنامج النصي مرة أخرى ، سترى خطأ في التحويل البرمجي:
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
ماذا حدث إذا قرأت البرنامج النصي من وجهة نظر المترجم ، يصبح واضحًا أنه لا يعرف شيئًا عن "النتيجة" المتغيرة. لكنك ، كمطور ، تعلم أن هذا متغير double
، لكن المترجم لا يمكنه إخراجه. لهذا الغرض ، يتم إنشاء ملحقات للتحقق من النوع: يمكنك إعطاء المترجم معلومات إضافية ، وستعمل الترجمة بشكل جيد. في هذه الحالة ، نحتاج إلى الإشارة إلى أن متغير score
من النوع double
.
لذلك ، يمكنك تغيير طريقة @CompileStatic
تعليق توضيحي @CompileStatic
:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension2.groovy")), CompileStatic.class);
هذا "يحاكي" الكود المشروح بواسطة @CompileStatic(extensions=['SecureExtension2.groovy'])
. الآن ، بالطبع ، نحتاج إلى كتابة ملحق يتعرف على متغير score
:
SecureExtension2.groovy
unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } }
- في حالة لا يمكن تحديد مدقق النوع المتغير
- إذا كان اسم المتغير هو
score
- اسمح للمترجم بتعريف المتغير بشكل حيوي بنوع
double
يمكن العثور على وصف كامل لملحقات DSL لفحص النوع في هذا القسم من الوثائق ، ولكن هناك مثال على وضع التحويل البرمجي المدمج: لا يمكن للمترجم تعريف متغير score
. أنت ، كمطور DSL ، تعلم أن المتغير هو في الواقع نوعه - double
، لذا فإن الدعوة إلى makeDynamic
هنا لتقول: "حسنًا ، لا تقلق ، أعرف ما أقوم به ، يمكن تعريف هذا المتغير ديناميكيًا بنوع double
". هذا كل شئ!
الانتهاء أولا التمديد "آمنة"
الآن دعونا نضع كل ذلك معا. لقد كتبنا ملحق فحص نوع واحد يمنع الاتصالات لطرق فئة System
من جهة والآخر الذي يحدد متغير score
ناحية أخرى. لذلك ، إذا وصلناهم ، سنحصل على أول ملحق كامل لفحص النوع:
SecureExtension3.groovy
تذكر أن تقوم بتحديث التكوين في فئة Java لاستخدام الملحق الجديد لفحص النوع:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension3.groovy")), CompileStatic.class);
قم بتشغيل الكود مرة أخرى - لا يزال يعمل. جرب هذا الآن:
abs(cos(1+score)) System.exit(-1)
سيتعطل تجميع البرنامج النصي مع وجود خطأ:
Script1.groovy: 1: [Static type checking] - Method call is not allowed! @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error
تهانينا ، لقد كتبت للتو ملحق التحقق من النوع الأول الذي يمنع تشغيل التعليمات البرمجية الضارة!
تعزيز التكوين التمديد
لذا ، كل شيء يسير على ما يرام ، يمكننا حظر المكالمات إلى أساليب فئة System
، ولكن يبدو أنه سيتم اكتشاف ثغرات أمنية جديدة في وقت قريب ، وسنحتاج إلى منع بدء تشغيل التعليمات البرمجية الضارة. لذلك بدلاً من تثبيت كل شيء في الملحق ، سنحاول جعل امتدادنا عالميًا وقابل للتخصيص. قد يكون هذا هو الأكثر صعوبة ، لأنه لا توجد طريقة مباشرة لتمرير السياق إلى ملحق فحص الكتابة. تستند الفكرة ، بالتالي ، إلى استخدام متغير محلي لمؤشر الترابط (طريقة المنحنى ، نعم) لتمرير بيانات التكوين إلى مدقق الكتابة.
بادئ ذي بدء ، سنجعل قائمة المتغيرات قابلة للتخصيص. هذا ما سيبدو عليه كود 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(); } } }
ThreadLocal
,- —
SecureExtension4.groovy
variableTypes
— “ → ”score
options
—- "variable types" VAR_TYPES
- thread local
- , , thread local
:
import static Sandbox.* def typesOfVariables = COMPILE_OPTIONS.get()[VAR_TYPES] unresolvedVariable { var -> if (typesOfVariables[var.name]) { return makeDynamic(var, typesOfVariables[var.name]) } }
- thread local
- , ,
- 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";
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]) } }
MethodNode
- thread local
- ,
, :
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() {
استنتاج
Groovy JVM. , . , , , . , Groovy, sandboxing' (, , ).
, , . , . , , .
, sandboxing', , — SecureASTCustomizer
. , , : secure AST customizer , (, ), ( , ).
, : , , . Groovy . Groovy, , - pull request, - !