
Depuis un traducteur: lors du développement de la plateforme CUBA, nous mettons dans ce cadre la possibilité d'exécuter des scripts personnalisés pour une configuration plus flexible de la logique métier de l'application. Que cette opportunité soit bonne ou mauvaise (et nous ne parlons pas seulement de CUBA) est débattue depuis longtemps, mais le fait que le contrôle de l'exécution des scripts utilisateur soit nécessaire ne soulève aucune question. L'une des fonctionnalités utiles de Groovy pour gérer l'exécution de scripts personnalisés est présentée dans cette traduction de Cédric Champeau. Malgré le fait qu'il ait récemment quitté l'équipe de développement de Groovy, la communauté des programmeurs semble profiter longtemps de ses travaux.
L'un des moyens les plus couramment utilisés pour utiliser Groovy consiste à utiliser des scripts, car Groovy facilite l'exécution dynamique de code lors de l'exécution. Selon l'application, les scripts peuvent être localisés à différents endroits: le système de fichiers, la base de données, les services distants ... mais surtout, le développeur de l'application exécutant les scripts ne les écrit pas forcément. De plus, les scripts peuvent fonctionner dans un environnement limité (mémoire limitée, limite sur le nombre de descripteurs de fichiers, runtime ...), ou vous pouvez empêcher l'utilisateur d'utiliser toutes les fonctionnalités du langage dans le script.
Ce message vous le dira.
- pourquoi groovy est bon pour écrire dsl interne
- quelles sont ses fonctionnalités en termes de sécurité de votre application
- comment configurer la compilation pour améliorer DSL
- sur la valeur de
SecureASTCustomizer
- Ă propos des extensions de contrĂ´le de type
- comment utiliser les extensions de contrĂ´le de type pour rendre le sandboxing efficace
Par exemple, imaginez ce que vous devez faire pour que l'utilisateur puisse calculer des expressions mathématiques. Une option d'implémentation consiste à incorporer un DSL interne, à créer un analyseur et enfin un interpréteur pour ces expressions. Pour ce faire, bien sûr, vous devrez travailler, mais si vous devez augmenter la productivité, par exemple, en générant du bytecode pour les expressions au lieu de les calculer dans l'interpréteur ou en utilisant la mise en cache des classes générées lors de l'exécution, alors Groovy est une excellente option.
Il existe de nombreuses options décrites dans la documentation , mais l'exemple le plus simple utilise simplement la classe Eval
:
Example.java
int sum = (Integer) Eval.me("1+1");
Le code 1+1
est analysé, compilé en bytecode, chargé et exécuté par Groovy en runtime. Bien sûr, le code de cet exemple est très simple et vous devrez ajouter des paramètres, mais l'idée est que le code exécutable peut être arbitraire. Et ce n'est peut-être pas exactement ce dont vous avez besoin. Dans la calculatrice, vous devez autoriser quelque chose comme ceci:
1+1 x+y 1+(2*x)**y cos(alpha)*r v=1+x
mais certainement pas
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!
C'est ici que les difficultés commencent, et il devient également clair que nous devons résoudre plusieurs problèmes:
- limiter la grammaire d'une langue à un sous-ensemble de ses capacités
- empêcher les utilisateurs d'exécuter le code
- empêcher l'exécution de code malveillant
L'exemple avec la calculatrice est assez simple, mais pour les DSL plus complexes, les gens peuvent ne pas remarquer qu'ils écrivent du code problématique, surtout si DSL est si simple que les développeurs ne peuvent pas l'utiliser.
Il y a quelques années, j'étais dans cette situation. J'ai développé un moteur qui exécutait des «scripts» Groovy écrits par des linguistes. Un problème, par exemple, était qu'ils pouvaient créer par inadvertance une boucle sans fin. Le code a été exécuté sur le serveur, et il est apparu un thread dévorant 100% du CPU, après quoi il fallait redémarrer le serveur d'applications. J'ai dû chercher un moyen de résoudre le problème sans affecter les performances DSL, les outils ou les applications.
En fait, de nombreuses personnes ont des besoins similaires. Au cours des 4 dernières années, j'ai parlé à beaucoup de gens qui ont eu la même question: comment puis-je empêcher les utilisateurs de faire des bêtises dans les scripts Groovy?
Compilateurs de personnalisation
À cette époque, j'avais déjà ma propre décision et je savais que d'autres personnes avaient également développé quelque chose de similaire. Au final, Guillaume Laforge m'a proposé de créer un mécanisme dans le noyau Groovy pour aider à résoudre ces problèmes. Il est apparu dans Groovy 1.8.0 en tant que personnalisateurs de compilation .
Les personnalisateurs de compilation sont un ensemble de classes qui modifient le processus de compilation des scripts Groovy. Vous pouvez écrire votre propre personnalisateur, mais Groovy fournit:
- personnalisateur d'importation qui ajoute implicitement des importations aux scripts afin que les utilisateurs n'aient pas besoin d'ajouter des descriptions d'importation
- personnalisation des transformations AST (arbre de syntaxe abstraite), vous permettant d'ajouter des transformations AST directement aux scripts
- Personnaliseur AST sécurisé restreignant les constructions grammaticales et syntaxiques d'un langage
Le personnalisateur des transformations AST m'a aidé à résoudre le problème de boucle sans fin avec la transformation @ThreadInterrupt
, mais SecureASTCustomizer est la chose qui est probablement la plus mal comprise dans la grande majorité des cas.
Je devrais m'en excuser. Ensuite, je ne pouvais pas trouver un meilleur nom. La partie la plus importante du nom «SecureASTCustomizer» est l' AST . Le but de ce mécanisme était de limiter l'accès à certaines fonctions AST. Le mot «sécurisé» dans le titre est généralement superflu, et j'expliquerai pourquoi. Il y a même un article de blog de Kosuke Kawaguchi, célèbre de Jenkins, intitulé «Fatal Groovy SecureASTCustomizer» . Et tout y est écrit très correctement. SecureASTCustomizer n'a pas été conçu pour le sandboxing. Il a été créé pour limiter le langage au moment de la compilation, mais pas lors de l'exécution. Maintenant, je pense que le meilleur nom serait GrammarCustomizer . Mais, comme vous le savez certainement, il y a trois difficultés en informatique: l'invalidation du cache, l'invention de noms et une erreur par unité.
Imaginez maintenant que vous envisagez de personnaliser le personnalisateur AST comme un moyen d'assurer la sécurité de votre script, et votre tâche consiste à empêcher l'utilisateur d' System.exit
partir du script. La documentation indique que les appels peuvent être interdits dans des récepteurs spéciaux en créant des listes noires ou blanches. Si la sécurité est nécessaire, je recommande toujours des listes blanches qui indiquent strictement ce qui est autorisé, mais pas des listes noires qui interdisent quoi que ce soit. Parce que les pirates pensent toujours à ce que vous n'auriez peut-être pas pensé. Je vais vous donner un exemple.
Voici comment configurer un moteur de script de bac Ă sable primitif Ă l'aide de SecureASTCustomizer
. Bien que je puisse les écrire dans Groovy, je donne des exemples de configuration Java pour rendre la différence entre le code d'intégration et les scripts plus explicites.
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); } }
- créer la configuration du compilateur
- créer un personnalisateur AST sécurisé
- déclarer que la classe
System
tant que récepteur d'appels de méthode est sur liste noire - ajouter un personnalisateur à la configuration du compilateur
- lier la configuration avec le script shell, c'est-à -dire essayer de créer un sandbox
- exécuter le "mauvais" script
- afficher le résultat de l'exécution du script
Si vous exécutez cette classe, une erreur se produira lors de l'exécution du script:
General error during canonicalization: Method calls not allowed on [java.lang.System] java.lang.SecurityException: Method calls not allowed on [java.lang.System]
Cette conclusion est émise par une application avec un personnalisateur AST sécurisé, qui ne permet pas l'exécution de méthodes de la classe System
. Succès! Nous avons donc protégé notre script! Mais attendez une minute ...
SecureASTCustomizer est piraté!
La protection, par exemple? Et si je fais ça:
def c = System c.exit(-1)
Si vous réexécutez le programme, vous verrez qu'il se bloque sans erreur et sans afficher le résultat à l'écran. Le code de sortie du processus est -1, ce qui signifie que le script utilisateur a été exécuté! Qu'est-il arrivé? Au moment de la compilation, le personnalisateur AST sécurisé n'est pas en mesure de reconnaître que c.exit
est un appel à la méthode System
en principe car il fonctionne au niveau AST! Il analyse l'appel de méthode, et dans ce cas, l'appel de méthode est c.exit(-1)
, puis il détermine le récepteur et vérifie s'il est dans la liste blanche (ou noire). Dans ce cas, le récepteur est c
, cette variable est déclarée via def , et cela revient à le déclarer en tant Object
, et le personnalisateur AST sécurisé pensera que le type de la variable c
est Object
, pas System
!
En général, il existe de nombreuses façons de contourner les différentes configurations créées sur le personnalisateur AST sécurisé. En voici quelques-unes:
((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)
et il peut y en avoir beaucoup plus. La nature dynamique de Groovy empêche la possibilité de résoudre ces problèmes au moment de la compilation. Cependant, une solution existe. Une option consiste à s'appuyer sur le gestionnaire de sécurité JVM standard. Cependant, il s'agit d'une solution lourde et volumineuse immédiatement pour l'ensemble du système, ce qui équivaut à tirer un canon sur des moineaux. De plus, cela ne fonctionne pas dans tous les cas, par exemple, si vous souhaitez interdire la lecture des fichiers, mais pas la création ...
Cette limitation - plutôt un chagrin pour beaucoup d'entre nous - a conduit à la création d'une solution basée sur les contrôles à l'exécution . Ce type de vérification n'a pas de tels problèmes. Par exemple, parce que vous connaîtrez le type de récepteur réel du message avant de commencer la validation de l'appel de méthode. Les implémentations suivantes sont particulièrement intéressantes:
Cependant, aucune de ces implémentations n'est complètement fiable et sûre. Par exemple, la version de Kosuke est basée sur le piratage de l'implémentation interne de la mise en cache du site d'appel. Le problème est qu'il n'est pas compatible avec la version dynamique invoquée de Groovy, et ces classes internes ne seront pas dans les futures versions de Groovy. La version de Simon, quant à elle, est basée sur des transformations AST, mais laisse de nombreux trous potentiels.
En conséquence, mes amis Corinne Crisch, Fabrice Matrat et Sebastian Blanc, et moi avons décidé de créer un nouveau mécanisme de sandboxing lors de l'exécution, qui n'aura pas de problèmes tels que ces projets. Nous avons commencé à le mettre en œuvre lors d'un hackathon à Nice, et lors de la conférence Greach de l'année dernière, nous avons fait un rapport à ce sujet . Ce mécanisme est basé sur des transformations AST et réécrit essentiellement le code à vérifier avant chaque appel de méthode, tenter d'accéder au champ de classe, incrémenter une variable, une expression binaire ... Cette implémentation n'est toujours pas prête, et peu de travail a été fait dessus, donc car je me suis rendu compte que le problème avec les méthodes et les paramètres appelés "implicitement ceci" n'a pas encore été résolu, comme, par exemple, dans les constructeurs:
xml { cars { // cars is a method call on an implicit this: "this".cars(...) car(make:'Renault', model: 'Clio') } }
À ce jour, je n'ai toujours pas trouvé de moyen de résoudre ce problème en raison de l'architecture du protocole de méta-objet dans Groovy, qui est basé sur le fait que le récepteur lève une exception lorsqu'il ne trouve pas la méthode, avant de passer à un autre récepteur. En bref, cela signifie que vous ne pouvez pas trouver le type de récepteur avant l'appel de méthode réel. Et si l'appel est passé, alors c'est trop tard ...
Et jusqu'à récemment, je n'avais pas de solution optimale à ce problème dans le cas où le script exécutable utilise les propriétés dynamiques du langage. Mais le moment est venu d'expliquer comment vous pouvez améliorer considérablement la situation si vous êtes prêt à sacrifier un peu le dynamisme de la langue.
Vérification de type
Revenons au problème principal avec SecureASTCustomizer: il fonctionne avec une arborescence de syntaxe abstraite et n'a pas d'informations sur les types de messages et les récepteurs spécifiques. Mais avec Groovy 2, Groovy a ajouté la compilation, et dans Groovy 2.1, nous avons ajouté des extensions pour la vérification de type .
Les extensions pour la vérification de type sont une chose très puissante: elles permettent au développeur Groovy DSL d'aider le compilateur avec l'inférence de type, et permettent également la génération d'erreurs de compilation dans les cas où elles ne se produisent généralement pas. Ces extensions sont utilisées en interne par Groovy pour prendre en charge un compilateur statique, par exemple, lors de l'implémentation de traits ou d'un moteur de modèle de balisage .
Et si, au lieu d'utiliser les résultats de l'analyseur, nous pouvions nous fier aux informations du mécanisme de vérification de type? Prenez le code que notre pirate a essayé d'écrire:
((Object)System).exit(-1)
Si vous activez les vérifications de type, le code ne compile pas:
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.
Donc, ce code ne compile plus. Et si on prend ce code:
def c = System c.exit(-1)
Comme vous pouvez le voir, il passe la vérification de type, enveloppé dans une méthode et exécuté à l'aide de la commande groovy
:
@groovy.transform.TypeChecked
Le vérificateur de type détecte que la méthode de exit
est appelée à partir de la classe System
et est valide. Cela ne nous aidera pas ici. Mais ce que nous savons, c'est que si ce code passe la vérification de type, cela signifie que le compilateur reconnaît l'appel au récepteur avec le type System
. En général, l'idée est d'interdire un appel avec une extension pour la vérification de type.
Extension simple pour la vérification de type
Avant de nous plonger dans le sandboxing en détail, essayons de «sécuriser» notre script à l'aide d'une extension standard pour la vérification de type. L'enregistrement d'une telle extension est facile: il suffit de définir le paramètre d' extensions
pour l'annotation @TypeChecked
(ou @CompileStatic
si vous utilisez une compilation statique):
@TypeChecked(extensions=['SecureExtension1.groovy']) void foo() { def c = System c.exit(-1) } foo()
La recherche d'extensions aura lieu dans le chemin de classe au format du code source (vous pouvez créer des extensions précompilées pour la vérification de type, mais nous ne les considérerons pas dans cet article):
SecureExtension1.groovy
onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } }
- lorsque le vérificateur de type sélectionne une méthode à appeler
- si la méthode appartient à la classe
System
- puis laissez le vérificateur de type générer une erreur
C’est tout ce dont vous avez besoin. Maintenant, exécutez à nouveau le code et vous verrez une erreur de compilation!
/home/cchampeau/tmp/securetest.groovy: 6: [Static type checking] - Method call is not allowed! @ line 6, column 3. c.exit(-1) ^ 1 error
Cette fois, grâce au vérificateur de type, c
reconnu comme une instance de la classe System
, et nous pouvons interdire l'appel. Il s'agit d'un exemple très simple, et il ne montre pas tout ce qui peut être fait avec le personnalisateur AST sécurisé en termes de configuration. Dans l'extension que nous avons écrite , les chèques sont codés en dur , mais il pourrait être préférable de les rendre personnalisables. Alors, compliquons l'exemple.
Supposons que votre application calcule certaines métriques d'un document et permette aux utilisateurs de les personnaliser. Dans ce cas, DSL:
- fonctionnera (au moins) la variable de
score
- permet aux utilisateurs d'effectuer des opérations mathématiques (y compris appeler les méthodes cos , abs , ...)
- doit interdire toutes les autres méthodes
Exemple de script utilisateur:
abs(cos(1+score))
Cette DSL est facile à configurer. Ceci est une variante de ce que nous avons défini ci-dessus:
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);
- ajouter un personnalisateur d'importation qui ajoutera l'
import static java.lang.Math.*
Ă tous les scripts - rendre la variable de
score
disponible pour le script - exécuter le script
Il existe des moyens de mettre en cache des scripts au lieu de les analyser et de les compiler à chaque fois. Consultez la documentation pour plus de détails.
Donc, notre script fonctionne, mais rien n'empêche le pirate de lancer du code malveillant. Puisque nous prévoyons d'utiliser la vérification de type, je recommanderais d'utiliser la transformation @CompileStatic
:
- il active la vérification de type dans le script, et nous pourrons effectuer des vérifications supplémentaires grâce à l'extension pour la vérification de type
- améliorer les performances des scripts
@CompileStatic
implicitement l'annotation @CompileStatic
Ă vos scripts est assez simple. Il vous suffit de mettre Ă jour la configuration du compilateur:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(CompileStatic.class); conf.addCompilationCustomizers(astcz);
Maintenant, si vous essayez de réexécuter le script, vous verrez une erreur de compilation:
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
Qu'est-il arrivé? Si vous lisez le script du point de vue du compilateur, il devient clair qu'il ne sait rien de la variable "score". Mais vous, en tant que développeur, savez qu'il s'agit d'une double
variable, mais le compilateur ne peut pas la produire. Pour cela, des extensions pour la vérification de type sont créées: vous pouvez donner au compilateur des informations supplémentaires, et la compilation fonctionnera bien. Dans ce cas, nous devons indiquer que la variable de score
est de type double
.
Par conséquent, vous pouvez légèrement modifier la façon dont l'annotation @CompileStatic
est @CompileStatic
:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension2.groovy")), CompileStatic.class);
Cela "émule" le code annoté par @CompileStatic(extensions=['SecureExtension2.groovy'])
. Maintenant, bien sûr, nous devons écrire une extension qui reconnaîtra la variable de score
:
SecureExtension2.groovy
unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } }
- dans le cas où le vérificateur de type ne peut pas déterminer la variable
- si le nom de la variable est
score
- laissez le compilateur définir la variable dynamiquement avec le type
double
Une description complète des extensions DSL pour la vérification de type peut être trouvée dans cette section de la documentation , mais il existe un exemple de mode de compilation combiné: le compilateur ne peut pas définir de variable de score
. En tant que développeur DSL, vous savez que la variable est en fait son type - double
, donc l'appel Ă makeDynamic
là pour dire: "ok, ne vous inquiétez pas, je sais ce que je fais, cette variable peut être définie dynamiquement avec le double
type " C'est tout!
Première extension "sécurisée" terminée
Maintenant, mettons tout cela ensemble. Nous avons écrit une extension de vérification de type qui empêche les appels aux méthodes de la classe System
une part et une autre qui définit la variable de score
de l'autre. Donc, si nous les connectons, nous obtenons la première extension complète pour la vérification de type:
SecureExtension3.groovy
N'oubliez pas de mettre à jour la configuration de votre classe Java pour utiliser la nouvelle extension pour la vérification de type:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension3.groovy")), CompileStatic.class);
Exécutez à nouveau le code - cela fonctionne toujours. Maintenant, essayez ceci:
abs(cos(1+score)) System.exit(-1)
La compilation du script se bloquera avec une erreur:
Script1.groovy: 1: [Static type checking] - Method call is not allowed! @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error
Félicitations, vous venez d'écrire la première extension de vérification de type qui empêche l'exécution de code malveillant!
Configuration d'extension améliorée
Donc, tout va bien, nous pouvons interdire les appels aux méthodes de la classe System
, mais il semble que de nouvelles vulnérabilités seront bientôt découvertes, et nous devrons empêcher le lancement de code malveillant. Ainsi, au lieu de tout coder en dur dans l'extension, nous allons essayer de rendre notre extension universelle et personnalisable. C'est probablement le plus difficile, car il n'y a aucun moyen direct de passer le contexte à l'extension pour la vérification de type. L'idée est donc basée sur l'utilisation d'une variable locale de thread (méthode de la courbe, oui) pour transmettre les données de configuration aux vérificateurs de type.
Tout d'abord, nous allons personnaliser la liste des variables. Voici Ă quoi ressemblera le code 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() {
Conclusion
Groovy JVM. , . , , , . , Groovy, sandboxing' (, , ).
, , . , . , , .
, sandboxing', , — SecureASTCustomizer
. , , : secure AST customizer , (, ), ( , ).
, : , , . Groovy . Groovy, , - pull request, - !