
Von einem Übersetzer: Bei der Entwicklung der CUBA-Plattform haben wir in dieses Framework die Möglichkeit integriert, benutzerdefinierte Skripts für eine flexiblere Konfiguration der Geschäftslogik der Anwendung auszuführen. Ob diese Gelegenheit gut oder schlecht ist (und wir sprechen nicht nur über CUBA), wird lange diskutiert, aber die Tatsache, dass die Kontrolle über die Ausführung von Benutzerskripten notwendig ist, wirft keine Fragen auf. Eine der nützlichen Funktionen von Groovy zum Verwalten der benutzerdefinierten Skriptausführung wird in dieser Übersetzung von Cédric Champeau vorgestellt. Trotz der Tatsache, dass er kürzlich das Groovy-Entwicklungsteam verlassen hat, scheint die Community der Programmierer seine Arbeit noch lange zu nutzen.
Eine der am häufigsten verwendeten Methoden zur Verwendung von Groovy ist die Skripterstellung, da Groovy die dynamische Ausführung von Code zur Laufzeit vereinfacht. Abhängig von der Anwendung können sich die Skripte an verschiedenen Orten befinden: im Dateisystem, in der Datenbank, in den Remotediensten ... aber am wichtigsten ist, dass der Entwickler der Anwendung, die die Skripte ausführt, sie nicht unbedingt schreibt. Darüber hinaus können Skripte in einer begrenzten Umgebung (begrenzter Speicher, Begrenzung der Anzahl der Dateideskriptoren, Laufzeit ...) ausgeführt werden, oder Sie möchten den Benutzer möglicherweise daran hindern, alle Sprachfunktionen des Skripts zu verwenden.
Dieser Beitrag wird es Ihnen sagen.
- warum groovy ist gut für das schreiben von internen dsl
- Was sind seine Funktionen in Bezug auf die Sicherheit Ihrer Anwendung
- Konfigurieren der Kompilierung zur Verbesserung von DSL
- über den Wert von
SecureASTCustomizer
- über Typsteuerungserweiterungen
- Verwendung von Typsteuerungserweiterungen, um Sandboxing effektiv zu machen
Stellen Sie sich beispielsweise vor, was Sie tun müssen, damit der Benutzer mathematische Ausdrücke berechnen kann. Eine Implementierungsoption besteht darin, ein internes DSL einzubetten, einen Parser und schließlich einen Interpreter für diese Ausdrücke zu erstellen. Dazu müssen Sie natürlich arbeiten. Wenn Sie jedoch die Produktivität steigern müssen, indem Sie beispielsweise Bytecode für Ausdrücke generieren, anstatt sie im Interpreter zu berechnen, oder das Zwischenspeichern von zur Laufzeit generierten Klassen verwenden, ist Groovy eine großartige Option.
In der Dokumentation werden viele Optionen beschrieben, aber das einfachste Beispiel ist die Verwendung der Eval
Klasse:
Example.java
int sum = (Integer) Eval.me("1+1");
Der 1+1
Code wird analysiert, zu Bytecode kompiliert, von Groovy zur Laufzeit geladen und ausgeführt. Natürlich ist der Code in diesem Beispiel sehr einfach und Sie müssen Parameter hinzufügen, aber die Idee ist, dass der ausführbare Code beliebig sein kann. Und das ist möglicherweise nicht genau das, was Sie brauchen. Im Taschenrechner müssen Sie Folgendes zulassen:
1+1 x+y 1+(2*x)**y cos(alpha)*r v=1+x
aber schon gar nicht
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!
Hier beginnen Schwierigkeiten, und es wird auch klar, dass wir mehrere Probleme lösen müssen:
- Beschränken Sie die Grammatik einer Sprache auf eine Teilmenge ihrer Fähigkeiten
- verhindern, dass Benutzer den Code ausführen
- Verhindern Sie die Ausführung von Schadcode
Das Taschenrechner-Beispiel ist recht einfach, aber bei komplexeren DSLs bemerken die Benutzer möglicherweise nicht, dass sie problematischen Code schreiben, insbesondere wenn DSL so einfach ist, dass es von Nicht-Entwicklern verwendet werden kann .
Vor ein paar Jahren war ich in dieser Situation. Ich habe eine Engine entwickelt, die Groovy-Skripte ausführt, die von Linguisten geschrieben wurden. Ein Problem war zum Beispiel, dass sie versehentlich eine Endlosschleife erzeugen konnten. Der Code wurde auf dem Server ausgeführt, und es wurde ein Thread angezeigt, der 100% der CPU verschlingt. Danach musste der Anwendungsserver neu gestartet werden. Ich musste nach einer Möglichkeit suchen, das Problem zu lösen, ohne die Leistung von DSL, Tools oder Anwendungen zu beeinträchtigen.
In der Tat haben viele Menschen ähnliche Bedürfnisse. In den letzten 4 Jahren habe ich mit vielen Leuten gesprochen, die die gleiche Frage hatten: Wie kann ich verhindern, dass Benutzer in Groovy-Skripten Unsinn machen?
Anpassungs-Compiler
Zu dieser Zeit hatte ich bereits meine eigene Entscheidung und ich wusste, dass auch andere Leute etwas Ähnliches entwickelten. Am Ende schlug Guillaume Laforge vor, einen Mechanismus im Groovy-Kernel zu erstellen, um diese Probleme zu lösen. Es erschien in Groovy 1.8.0 als Compilation-Customizer .
Kompilierungsanpassungsprogramme sind eine Reihe von Klassen, die den Kompilierungsprozess von Groovy-Skripten ändern. Sie können Ihren eigenen Customizer schreiben, aber Groovy liefert:
- Import-Customizer, der Skripten implizit Importe hinzufügt, sodass Benutzer keine Importbeschreibungen hinzufügen müssen
- Customizer-AST-Transformationen (Abstract Syntax Tree), mit denen Sie AST-Transformationen direkt zu Skripten hinzufügen können
- Sicherer AST-Customizer, der Grammatik- und Syntaxkonstrukte einer Sprache einschränkt
Der Customizer von AST-Transformationen hat mir geholfen, das Endlosschleifenproblem mit der @ThreadInterrupt
Transformation zu lösen, aber SecureASTCustomizer wird in den allermeisten Fällen wahrscheinlich am meisten missverstanden.
Ich sollte mich dafür entschuldigen. Dann könnte ich mir keinen besseren Namen einfallen lassen. Der wichtigste Teil im Namen „SecureASTCustomizer“ ist der AST . Der Zweck dieses Mechanismus bestand darin, den Zugriff auf bestimmte AST-Funktionen zu beschränken. Das Wort "sicher" im Titel ist im Allgemeinen überflüssig, und ich werde erklären, warum. Es gibt sogar einen Blog-Beitrag von Jenkins berühmtem Kosuke Kawaguchi mit dem Titel „Fatal Groovy SecureASTCustomizer“ . Und dort ist alles sehr richtig geschrieben. SecureASTCustomizer wurde nicht für Sandboxing entwickelt. Es wurde erstellt, um die Sprache beim Kompilieren einzuschränken, jedoch nicht die Ausführung. Jetzt denke ich, der beste Name wäre GrammarCustomizer . Wie Sie sicherlich wissen, gibt es in der Informatik drei Schwierigkeiten: die Ungültigmachung des Caches, das Erfinden von Namen und einen Fehler pro Einheit.
Stellen Sie sich nun vor, Sie erwägen einen sicheren AST-Customizer, um die Sicherheit Ihres Skripts zu gewährleisten, und Ihre Aufgabe besteht darin, zu verhindern, dass der Benutzer System.exit
über das Skript System.exit
. Die Dokumentation besagt, dass Anrufe in speziellen Empfängern durch Erstellen von schwarzen oder weißen Listen gesperrt werden können. Wenn Sicherheit benötigt wird, empfehle ich immer Whitelists, die genau angeben, was erlaubt ist, aber keine Blacklists, die irgendetwas verbieten. Weil Hacker immer darüber nachdenken, was Sie vielleicht nicht in Betracht gezogen haben. Ich werde ein Beispiel geben.
Hier SecureASTCustomizer
, wie Sie mit SecureASTCustomizer
eine primitive Sandbox-Skript-Engine SecureASTCustomizer
. Obwohl ich sie in Groovy schreiben könnte, gebe ich Java-Konfigurationsbeispiele, um den Unterschied zwischen Integrationscode und Skripten deutlicher zu machen.
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); } }
- Compilerkonfiguration erstellen
- Erstellen Sie einen sicheren AST-Customizer
- Deklarieren Sie, dass die
System
als Empfänger von Methodenaufrufen auf der schwarzen Liste steht - Fügen Sie der Compilerkonfiguration einen Customizer hinzu
- Binden Sie die Konfiguration mit dem Shell-Skript, dh versuchen Sie, eine Sandbox zu erstellen
- Führen Sie das "schlechte" Skript aus
- Zeigen Sie das Ergebnis der Ausführung des Skripts an
Wenn Sie diese Klasse ausführen, tritt während der Skriptausführung ein Fehler auf:
General error during canonicalization: Method calls not allowed on [java.lang.System] java.lang.SecurityException: Method calls not allowed on [java.lang.System]
Diese Schlussfolgerung wird von einer Anwendung mit einem sicheren AST-Customizer ausgegeben, der die Ausführung von Methoden der Systemklasse nicht zulässt. Erfolg! Also haben wir unser Skript geschützt! Aber warte mal ...
SecureASTCustomizer wird gehackt!
Schutz, sagen wir? Aber was ist, wenn ich das mache:
def c = System c.exit(-1)
Wenn Sie das Programm erneut ausführen, werden Sie feststellen, dass es ohne Fehler und ohne Anzeige des Ergebnisses auf dem Bildschirm abstürzt. Der Prozess-Exit-Code lautet -1, was bedeutet, dass das Benutzerskript ausgeführt wurde! Was ist passiert? Zum Zeitpunkt der Kompilierung kann der sichere AST-Customizer nicht erkennen, dass c.exit
im Prinzip ein Aufruf der System
, da er auf AST-Ebene funktioniert! Es analysiert den Methodenaufruf und in diesem Fall lautet der Methodenaufruf c.exit(-1)
. Anschließend wird der Empfänger ermittelt und geprüft, ob er in der weißen (oder schwarzen) Liste enthalten ist. In diesem Fall ist der Empfänger c
, diese Variable wird über def deklariert , und dies entspricht der Deklaration als Object
, und der sichere AST-Customizer glaubt, dass der Typ der Variablen c
Object
und nicht System
!
Im Allgemeinen gibt es viele Möglichkeiten, um die verschiedenen Konfigurationen zu umgehen, die im sicheren AST-Customizer erstellt wurden. Hier sind einige coole:
((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)
und es kann noch viel mehr geben. Die Dynamik von Groovy schließt die Möglichkeit aus, diese Probleme beim Kompilieren zu beheben. Es gibt jedoch eine Lösung. Eine Möglichkeit besteht darin, sich auf den Standard-JVM-Sicherheitsmanager zu verlassen. Dies ist jedoch sofort eine schwere und voluminöse Lösung für das gesamte System, und dies entspricht dem Abfeuern einer Kanone auf Spatzen. Darüber hinaus funktioniert es nicht in allen Fällen, wenn Sie beispielsweise das Lesen von Dateien verbieten, aber nicht ...
Diese Einschränkung - für viele von uns eher ein Ärger - führte zur Schaffung einer Lösung, die auf Überprüfungen zur Laufzeit basiert. Diese Art der Prüfung weist keine derartigen Probleme auf. Zum Beispiel, weil Sie den tatsächlichen Empfängertyp der Nachricht kennen, bevor Sie mit der Validierung des Methodenaufrufs beginnen. Von besonderem Interesse sind folgende Implementierungen:
Keine dieser Implementierungen ist jedoch absolut zuverlässig und sicher. Zum Beispiel basiert die Version von Kosuke auf dem Hack der internen Implementierung der Caching-Call-Site. Das Problem ist, dass es nicht mit der aufgerufenen dynamischen Version von Groovy kompatibel ist und diese inneren Klassen in zukünftigen Versionen von Groovy nicht mehr vorhanden sein werden. Simons Version hingegen basiert auf AST-Transformationen, hinterlässt jedoch viele potenzielle Lücken.
Infolgedessen haben meine Freunde Corinne Crisch, Fabrice Matrat und Sebastian Blanc und ich beschlossen, zur Laufzeit einen neuen Sandbox-Mechanismus zu erstellen, der keine Probleme wie diese Projekte haben wird. Wir haben bei einem Hackathon in Nizza damit begonnen, und auf der Greach-Konferenz im letzten Jahr haben wir darüber berichtet . Dieser Mechanismus basiert auf AST-Transformationen und schreibt den zu überprüfenden Code vor jedem Methodenaufruf im Wesentlichen neu, versucht, auf das Klassenfeld zuzugreifen, erhöht eine Variable, einen binären Ausdruck ... Diese Implementierung ist noch nicht fertig und es wurde nicht viel daran gearbeitet als ich feststellte, dass das Problem mit Methoden und Parametern, die durch "implicit this" aufgerufen werden, noch nicht gelöst wurde, wie zum Beispiel bei Buildern:
xml { cars { // cars is a method call on an implicit this: "this".cars(...) car(make:'Renault', model: 'Clio') } }
Bisher habe ich aufgrund der Architektur des Metaobjektprotokolls in Groovy, die auf der Tatsache basiert, dass der Empfänger eine Ausnahme auslöst, wenn er die Methode nicht finden kann, noch keine Möglichkeit gefunden, dieses Problem zu lösen, bevor er zu einem anderen Empfänger wechselt. Kurz gesagt bedeutet dies, dass Sie den Empfängertyp vor dem eigentlichen Methodenaufruf nicht herausfinden können. Und wenn der Anruf bestanden hat, ist es zu spät ...
Und bis vor kurzem hatte ich keine optimale Lösung für dieses Problem für den Fall, dass das ausführbare Skript die dynamischen Eigenschaften der Sprache verwendet. Jetzt ist es an der Zeit zu erklären, wie Sie die Situation erheblich verbessern können, wenn Sie bereit sind, ein wenig von der Dynamik der Sprache zu opfern.
Typprüfung
Kehren wir zum Hauptproblem mit SecureASTCustomizer zurück: Es funktioniert mit einem abstrakten Syntaxbaum und enthält keine Informationen zu bestimmten Nachrichtentypen und Empfängern. Mit Groovy 2 hat Groovy die Kompilierung hinzugefügt, und in Groovy 2.1 haben wir Erweiterungen für die Typprüfung hinzugefügt.
Erweiterungen für die Typprüfung sind sehr leistungsfähig: Sie ermöglichen es dem Groovy DSL-Entwickler, dem Compiler bei der Typinferenz zu helfen, und ermöglichen auch die Generierung von Kompilierungsfehlern in Fällen, in denen sie normalerweise nicht auftreten. Diese Erweiterungen werden von Groovy intern verwendet, um einen statischen Compiler zu unterstützen, beispielsweise bei der Implementierung von Merkmalen oder einer Markup-Template-Engine .
Was wäre, wenn wir uns anstelle der Ergebnisse des Parsers auf Informationen aus dem Typprüfungsmechanismus verlassen könnten? Nehmen Sie den Code, den unser Hacker zu schreiben versucht hat:
((Object)System).exit(-1)
Wenn Sie Typprüfungen aktivieren, wird der Code nicht kompiliert:
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.
Dieser Code wird also nicht mehr kompiliert. Und was ist, wenn wir diesen Code nehmen:
def c = System c.exit(-1)
Wie Sie sehen können, besteht es die Typprüfung, wird in eine Methode eingeschlossen und mit dem Befehl groovy
ausgeführt:
@groovy.transform.TypeChecked
Die Typprüfung erkennt, dass die exit
Methode von der System
Klasse aufgerufen wird und gültig ist. Dies wird uns hier nicht helfen. Wir wissen jedoch, dass wenn dieser Code die Typprüfung besteht, der Compiler den Anruf an den Empfänger mit dem Typ System
erkennt. Im Allgemeinen besteht die Idee darin, einen Anruf mit einer Nebenstelle zur Typprüfung zu verbieten.
Einfache Erweiterung zur Typprüfung
Bevor wir uns ausführlich mit Sandboxing befassen, versuchen wir, unser Skript mit Hilfe einer Standarderweiterung für die Typprüfung zu "sichern". Das Registrieren einer solchen Erweiterung ist einfach: @CompileStatic
Sie einfach den extensions
für die Annotation @TypeChecked
(oder @CompileStatic
wenn Sie die statische Kompilierung verwenden):
@TypeChecked(extensions=['SecureExtension1.groovy']) void foo() { def c = System c.exit(-1) } foo()
Die Erweiterungssuche findet im Klassenpfad im Quellcodeformat statt (Sie können vorkompilierte Erweiterungen für die Typprüfung erstellen, diese werden in diesem Artikel jedoch nicht berücksichtigt):
SecureExtension1.groovy
onMethodSelection { expr, methodNode -> if (methodNode.declaringClass.name=='java.lang.System') { addStaticTypeError("Method call is not allowed!", expr) } }
- Wenn die Typprüfung eine aufzurufende Methode auswählt
- wenn die Methode zur Klasse
System
- Lassen Sie dann die Typprüfung einen Fehler generieren
Das ist alles was Sie brauchen. Führen Sie nun den Code erneut aus und Sie werden einen Kompilierungsfehler sehen!
/home/cchampeau/tmp/securetest.groovy: 6: [Static type checking] - Method call is not allowed! @ line 6, column 3. c.exit(-1) ^ 1 error
Dieses Mal wird c
dank der Typprüfung als Instanz der Systemklasse erkannt, und wir können den Aufruf verbieten. Dies ist ein sehr einfaches Beispiel, und es zeigt nicht alles, was mit dem sicheren AST-Customizer in Bezug auf die Konfiguration möglich ist. In der Erweiterung, die wir geschrieben haben , sind die Prüfungen fest codiert , aber es ist möglicherweise besser, sie anpassbar zu machen. Lassen Sie uns das Beispiel komplizierter machen.
Angenommen, Ihre Anwendung berechnet bestimmte Metriken für ein Dokument und ermöglicht es Benutzern, diese anzupassen. In diesem Fall DSL:
- wird (mindestens) die
score
Variable bedienen - ermöglicht Benutzern das Ausführen mathematischer Operationen (einschließlich des Aufrufs von cos- , abs- , ... -Methoden)
- muss alle anderen Methoden verbieten
Beispiel für ein Benutzerskript:
abs(cos(1+score))
Dieses DSL ist einfach zu konfigurieren. Dies ist eine Variante dessen, was wir oben definiert haben:
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);
- Fügen Sie den Import-Customizer hinzu, der allen Skripten den
import static java.lang.Math.*
hinzufügt score
Variable für Skript verfügbar machen- Skript ausführen
Es gibt Möglichkeiten, Skripte zwischenzuspeichern, anstatt sie jedes Mal zu analysieren und zu kompilieren. Einzelheiten finden Sie in der Dokumentation.
Unser Skript funktioniert also, aber nichts hindert den Hacker daran, schädlichen Code zu starten. Da wir die Typprüfung verwenden möchten, würde ich die Verwendung der @CompileStatic
Transformation empfehlen:
- Es aktiviert die Typprüfung im Skript, und dank der Erweiterung für die Typprüfung können wir zusätzliche Prüfungen durchführen
- Verbessern Sie die Skriptleistung
Das implizite Hinzufügen der Annotation @CompileStatic
zu Ihren Skripten ist ziemlich einfach. Sie müssen nur die Compilerkonfiguration aktualisieren:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer(CompileStatic.class); conf.addCompilationCustomizers(astcz);
Wenn Sie nun versuchen, das Skript erneut auszuführen, wird ein Kompilierungsfehler angezeigt:
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
Was ist passiert? Wenn Sie das Skript aus Sicht des Compilers lesen, wird klar, dass er nichts über die Variable "score" weiß. Als Entwickler wissen Sie jedoch, dass dies eine double
ist, die der Compiler jedoch nicht ausgeben kann. Dazu werden Erweiterungen für die Typprüfung erstellt: Sie können dem Compiler zusätzliche Informationen geben, und die Kompilierung funktioniert einwandfrei. In diesem Fall müssen wir angeben, dass die score
vom Typ double
.
Daher können Sie die Art und Weise, @CompileStatic
Annotation @CompileStatic
geringfügig ändern:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension2.groovy")), CompileStatic.class);
Dies "emuliert" den von @CompileStatic(extensions=['SecureExtension2.groovy'])
Anmerkungen versehenen @CompileStatic(extensions=['SecureExtension2.groovy'])
. Jetzt müssen wir natürlich eine Erweiterung schreiben, die die score
erkennt:
SecureExtension2.groovy
unresolvedVariable { var -> if (var.name=='score') { return makeDynamic(var, double_TYPE) } }
- falls die Typprüfung die Variable nicht ermitteln kann
- wenn der Variablenname
score
- Lassen Sie den Compiler die Variable dynamisch mit dem Typ
double
Eine vollständige Beschreibung der DSL-Erweiterungen für die Typprüfung finden Sie in diesem Abschnitt der Dokumentation . Es gibt jedoch ein Beispiel für einen kombinierten Kompilierungsmodus: Der Compiler kann keine score
definieren. Als DSL-Entwickler wissen Sie, dass die Variable tatsächlich vom Typ double
ist. Der Aufruf von makeDynamic
daher: "Okay, keine Sorge, ich weiß, was ich tue. Diese Variable kann dynamisch mit dem Typ double
definiert werden." "" Das ist alles!
Erste abgeschlossene "sichere" Erweiterung
Lassen Sie uns jetzt alles zusammenfügen. Wir haben eine Typprüfungserweiterung geschrieben, die einerseits das Aufrufen von Methoden der Systemklasse verhindert, und eine andere, die andererseits die score
definiert. Wenn wir sie verbinden, erhalten wir die erste vollständige Erweiterung für die Typprüfung:
SecureExtension3.groovy
Denken Sie daran, die Konfiguration in Ihrer Java-Klasse zu aktualisieren, um die neue Erweiterung für die Typprüfung zu verwenden:
ASTTransformationCustomizer astcz = new ASTTransformationCustomizer( singletonMap("extensions", singletonList("SecureExtension3.groovy")), CompileStatic.class);
Führen Sie den Code erneut aus - es funktioniert immer noch. Versuchen Sie nun Folgendes:
abs(cos(1+score)) System.exit(-1)
Die Kompilierung des Skripts stürzt mit einem Fehler ab:
Script1.groovy: 1: [Static type checking] - Method call is not allowed! @ line 1, column 19. abs(cos(1+score));System.exit(-1) ^ 1 error
Herzlichen Glückwunsch, Sie haben gerade die erste Erweiterung zur Typprüfung geschrieben, die verhindert, dass bösartiger Code ausgeführt wird!
Erweiterte Erweiterungskonfiguration
Also läuft alles gut, wir können Aufrufe von Methoden der System
verbieten, aber es scheint, dass bald neue Schwachstellen entdeckt werden und wir den Start von Schadcode verhindern müssen. Anstatt alles in der Erweiterung fest zu codieren, werden wir versuchen, unsere Erweiterung universell und anpassbar zu machen. Dies ist wahrscheinlich die schwierigste, da es keine direkte Möglichkeit gibt, den Kontext zur Typprüfung an die Erweiterung zu übergeben. Die Idee basiert daher auf der Verwendung einer lokalen Thread-Variablen (Kurvenmethode, ja), um Konfigurationsdaten an Typprüfer zu übergeben.
Zunächst werden wir die Liste der Variablen anpassbar machen. So sieht der Java-Code aus:
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() {
Fazit
Groovy JVM. , . , , , . , Groovy, sandboxing' (, , ).
, , . , . , , .
, sandboxing', , — SecureASTCustomizer
. , , : secure AST customizer , (, ), ( , ).
, : , , . Groovy . Groovy, , - pull request, - !