Sandboxing mejorado para scripts geniales


De un traductor: Al desarrollar la Plataforma CUBA, ponemos en este marco la capacidad de ejecutar scripts personalizados para una configuración más flexible de la lógica empresarial de la aplicación. Si esta oportunidad es buena o mala (y estamos hablando no solo de CUBA) se debate durante mucho tiempo, sino que el hecho de que el control sobre la ejecución de los scripts de usuario sea necesario no plantea dudas. Una de las características útiles de Groovy para administrar la ejecución de scripts personalizados se presenta en esta traducción de Cédric Champeau. A pesar de que recientemente dejó el equipo de desarrollo de Groovy, la comunidad de programadores parece estar aprovechando su trabajo durante mucho tiempo.


Una de las formas más utilizadas para usar Groovy es a través de secuencias de comandos, ya que Groovy facilita la ejecución dinámica de código en tiempo de ejecución. Dependiendo de la aplicación, los scripts se pueden ubicar en diferentes lugares: el sistema de archivos, la base de datos, los servicios remotos ... pero lo más importante, el desarrollador de la aplicación que ejecuta los scripts no necesariamente los escribe. Además, los scripts pueden funcionar en un entorno limitado (memoria limitada, límite en la cantidad de descriptores de archivos, tiempo de ejecución ...), o puede querer evitar que el usuario use todas las funciones de lenguaje en el script.


Esta publicación te lo dirá.


  • por qué groovy es bueno para escribir dsl interno
  • ¿Cuáles son sus características en términos de seguridad de su aplicación?
  • Cómo configurar la compilación para mejorar DSL
  • sobre el valor de SecureASTCustomizer
  • acerca de las extensiones de control de tipo
  • cómo usar extensiones de control de tipo para hacer efectivo el sandboxing

Por ejemplo, imagine lo que necesita hacer para que el usuario pueda calcular expresiones matemáticas. Una opción de implementación es incrustar un DSL interno, crear un analizador y finalmente un intérprete para estas expresiones. Para hacer esto, por supuesto, tendrá que trabajar, pero si necesita aumentar la productividad, por ejemplo, generando bytecode para expresiones en lugar de calcularlas en el intérprete o usando el almacenamiento en caché de clases generadas en tiempo de ejecución, entonces Groovy es una gran opción.


Hay muchas opciones descritas en la documentación , pero el ejemplo más simple es usar la clase Eval :


Example.java


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

El código 1+1 es analizado, compilado en bytecode, cargado y ejecutado por Groovy en tiempo de ejecución. Por supuesto, el código en este ejemplo es muy simple, y necesitará agregar parámetros, pero la idea es que el código ejecutable puede ser arbitrario. Y eso puede no ser exactamente lo que necesita. En la calculadora debe permitir algo como esto:


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

pero ciertamente no


 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! 

Es aquí donde comienzan las dificultades, y también queda claro que necesitamos resolver varios problemas:


  • limitar la gramática de un idioma a un subconjunto de sus capacidades
  • evitar que los usuarios ejecuten el código que no se proporciona
  • evitar la ejecución de código malicioso

El ejemplo de la calculadora es bastante simple, pero para DSL más complejas, las personas pueden no darse cuenta de que están escribiendo código problemático, especialmente si DSL es tan simple que puede ser utilizado por personas que no son desarrolladores .


Hace unos años estaba en esta situación. Desarrollé un motor que ejecutaba "guiones" Groovy escritos por lingüistas. Un problema, por ejemplo, era que sin darse cuenta podían crear un bucle sin fin. El código se ejecutó en el servidor y apareció un subproceso que devoraba el 100% de la CPU, después de lo cual fue necesario reiniciar el servidor de aplicaciones. Tuve que buscar una manera de resolver el problema sin afectar el DSL, las herramientas o el rendimiento de la aplicación.


De hecho, muchas personas tienen necesidades similares. En los últimos 4 años, he estado hablando con muchas personas que han tenido la misma pregunta: ¿cómo puedo evitar que los usuarios hagan tonterías en los scripts Groovy?


Compiladores de personalización


En ese momento, ya tenía mi propia decisión y sabía que otras personas también desarrollaron algo similar. Al final, Guillaume Laforge sugirió que creara un mecanismo en el núcleo Groovy para ayudar a resolver estos problemas. Apareció en Groovy 1.8.0 como personalizadores de compilación .


Los personalizadores de compilación son un conjunto de clases que modifican el proceso de compilación de los scripts Groovy. Puede escribir su propio personalizador, pero Groovy proporciona:


  • Importar personalizador que implícitamente agrega importaciones a los scripts para que los usuarios no necesiten agregar descripciones de importación
  • Personalizar transformaciones AST (Árbol de sintaxis abstracta), lo que le permite agregar transformaciones AST directamente a los scripts
  • Personalizador AST seguro que restringe las construcciones de gramática y sintaxis de un idioma

El personalizador de las transformaciones AST me ayudó a resolver el problema del bucle sin fin con la transformación @ThreadInterrupt , pero SecureASTCustomizer es lo que probablemente se malinterpreta en la gran mayoría de los casos.


Debería disculparme por eso. Entonces no pude encontrar un mejor nombre. La parte más importante del nombre "SecureASTCustomizer" es el AST . El propósito de este mecanismo era limitar el acceso a ciertas funciones AST. La palabra "seguro" en el título es generalmente superflua, y explicaré por qué. Incluso hay una publicación de blog del famoso Kosuke Kawaguchi de Jenkins, titulada "Fatal Groovy SecureASTCustomizer" . Y todo está escrito muy correctamente allí. SecureASTCustomizer no fue diseñado para sandboxing. Fue creado para limitar el idioma en tiempo de compilación, pero no la ejecución. Ahora creo que el mejor nombre sería GrammarCustomizer . Pero, como seguramente sabe, hay tres dificultades en informática: invalidación de caché, inventar nombres y un error por unidad.


Ahora imagine que está considerando el personalizador AST seguro como un medio para garantizar la seguridad de su script, y su tarea es evitar que el usuario System.exit desde el script. La documentación dice que las llamadas pueden prohibirse en receptores especiales creando listas negras o blancas. Si se necesita seguridad, siempre recomiendo listas blancas que indiquen estrictamente lo que está permitido, pero no listas negras que prohíban cualquier cosa. Porque los hackers siempre piensan en lo que quizás no hayas considerado. Daré un ejemplo.


Aquí le mostramos cómo configurar un motor de script de sandbox primitivo con SecureASTCustomizer . Aunque podría escribirlos en Groovy, doy ejemplos de configuración de Java para que la diferencia entre el código de integración y los scripts sea más explícita.


 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. crear configuración del compilador
  2. crear un personalizador AST seguro
  3. declarar que la clase del System como receptor de llamadas a métodos está en la lista negra
  4. agregar personalizador a la configuración del compilador
  5. enlazar la configuración con el script de shell, es decir, intentar crear un sandbox
  6. ejecutar el script "malo"
  7. muestra el resultado de ejecutar el script

Si ejecuta esta clase, se producirá un error durante la ejecución del script:


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

Esta conclusión es emitida por una aplicación con un personalizador AST seguro, que no permite la ejecución de métodos de la clase System . Éxito! ¡Así que hemos protegido nuestro guión! Pero espera un minuto ...


¡SecureASTCustomizer está pirateado!


Protección, digamos? Pero qué pasa si hago esto:


 def c = System c.exit(-1) 

Si vuelve a ejecutar el programa, verá que se bloquea sin error y sin mostrar el resultado en la pantalla. El código de salida del proceso es -1, lo que significa que se ha ejecutado el script de usuario. Que paso En el momento de la compilación, el personalizador seguro de AST no puede reconocer que c.exit es, en principio, una llamada al método del System porque funciona en el nivel de AST. Analiza la llamada al método, y en este caso, la llamada al método es c.exit(-1) , luego determina el receptor y verifica si está en la lista blanca (o negra). En este caso, el receptor es c , esta variable se declara a través de def , y esto es lo mismo que declararlo como un Object , y el personalizador AST seguro pensará que el tipo de la variable c es Object , ¡no System !


En general, hay muchas formas de sortear las diversas configuraciones creadas en el personalizador AST seguro. Aquí hay algunos geniales:


 ((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) 

y puede haber muchos más La naturaleza dinámica de Groovy impide la capacidad de solucionar estos problemas en tiempo de compilación. Sin embargo, existe una solución. Una opción es confiar en el administrador de seguridad JVM estándar. Sin embargo, esta es una solución pesada y voluminosa de inmediato para todo el sistema, y ​​esto es equivalente a disparar un cañón a los gorriones. Además, no funciona en todos los casos, por ejemplo, si desea prohibir la lectura de archivos, pero no crear ...


Esta limitación, más bien un disgusto para muchos de nosotros, llevó a la creación de una solución basada en controles en tiempo de ejecución . Este tipo de verificación no tiene tales problemas. Por ejemplo, porque sabrá el tipo de receptor real del mensaje antes de comenzar la validación de la llamada al método. De particular interés son las siguientes implementaciones:



Sin embargo, ninguna de estas implementaciones es completamente confiable y segura. Por ejemplo, la versión de Kosuke se basa en el pirateo de la implementación interna del sitio de llamadas en caché. El problema es que no es compatible con la versión dinámica invocada de Groovy, y estas clases internas no estarán en versiones futuras de Groovy. La versión de Simon, por otro lado, se basa en transformaciones AST, pero deja muchos agujeros potenciales.


Como resultado, mis amigos Corinne Crisch, Fabrice Matrat y Sebastian Blanc, y yo decidimos crear un nuevo mecanismo de sandboxing en tiempo de ejecución, que no tendrá problemas como estos proyectos. Comenzamos a implementarlo en un hackathon en Niza, y en la conferencia de Greach el año pasado hicimos un informe al respecto . Este mecanismo se basa en las transformaciones AST y esencialmente reescribe el código para verificar antes de cada llamada al método, intentar acceder al campo de clase, incrementar una variable, expresión binaria ... Esta implementación aún no está lista, y no se ha hecho mucho trabajo al respecto, por lo que como me di cuenta de que el problema con los métodos y parámetros llamados a través de "esto implícito" aún no se ha resuelto, como, por ejemplo, en los constructores:


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

Hasta la fecha, todavía no he encontrado una manera de resolver este problema debido a la arquitectura del protocolo de metaobjetos en Groovy, que se basa en el hecho de que el receptor lanza una excepción cuando no puede encontrar el método antes de cambiar a otro receptor. En resumen, esto significa que no puede averiguar el tipo de receptor antes de la llamada al método real. Y si la llamada ha pasado, entonces es demasiado tarde ...


Y hasta hace poco, no tenía una solución óptima para este problema para el caso en que el script ejecutable usa las propiedades dinámicas del lenguaje. Pero ahora es el momento de explicar cómo puede mejorar significativamente la situación si está dispuesto a sacrificar un poco el dinamismo del lenguaje.


Comprobación de tipo


Volvamos al problema principal con SecureASTCustomizer: funciona con un árbol de sintaxis abstracta y no tiene información sobre tipos de mensajes y receptores específicos. Pero con Groovy 2, Groovy ha agregado compilación, y en Groovy 2.1 hemos agregado extensiones para la verificación de tipos .


Las extensiones para la verificación de tipos son muy poderosas: permiten que el desarrollador de Groovy DSL ayude al compilador con la inferencia de tipos, y también permiten la generación de errores de compilación en los casos en que generalmente no ocurren. Groovy utiliza estas extensiones internamente para admitir un compilador estático, por ejemplo, al implementar rasgos o un motor de plantillas de marcado .


¿Qué pasaría si, en lugar de utilizar los resultados del analizador, pudiéramos confiar en la información del mecanismo de verificación de tipo? Tome el código que nuestro hacker intentó escribir:


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


Si activa las verificaciones de tipo, el código no compila:


 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. 

Entonces este código ya no se compila. Y qué pasa si tomamos este código:


 def c = System c.exit(-1) 

Como puede ver, pasa la verificación de tipo, se envuelve en un método y se ejecuta utilizando el comando groovy :


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

El verificador de tipo detecta que el método de exit se llama desde la clase System y es válido. Esto no nos ayudará aquí. Pero lo que sabemos es que si este código pasa la verificación de tipo, significa que el compilador reconoce la llamada al receptor con el tipo System . En general, la idea es prohibir una llamada con una extensión para la verificación de tipos.


Extensión simple para verificación de tipo


Antes de profundizar en el sandboxing en detalle, intentemos "asegurar" nuestro script con la ayuda de una extensión estándar para la verificación de tipos. Registrar una extensión de este tipo es fácil: simplemente configure el parámetro de extensions para la anotación @TypeChecked (o @CompileStatic si está utilizando una compilación estática):


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

La búsqueda de extensiones se llevará a cabo en el classpath en el formato de código fuente (puede hacer extensiones precompiladas para la verificación de tipos, pero no las consideraremos en este artículo):


SecureExtension1.groovy


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

  1. cuando el verificador de tipos selecciona un método para llamar
  2. si el método pertenece a la clase System
  3. luego deje que el verificador de tipos genere un error

Eso es todo lo que necesitas. ¡Ahora ejecute el código nuevamente y verá un error de compilación!


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

Esta vez, gracias al comprobador de tipos, c reconoce como una instancia de la clase System , y podemos prohibir la llamada. Este es un ejemplo muy simple y no demuestra todo lo que se puede hacer con el personalizador AST seguro en términos de configuración. En la extensión que escribimos , los cheques están codificados , pero podría ser mejor hacerlos personalizables. Así que hagamos el ejemplo más complicado.


Suponga que su aplicación calcula ciertas métricas para un documento y permite a los usuarios personalizarlas. En este caso, DSL:


  • operará (al menos) la variable de score
  • permite a los usuarios realizar operaciones matemáticas (incluidos los métodos cos , abs , ...)
  • debe prohibir todos los demás métodos

Script de usuario de muestra:


abs(cos(1+score))


Este DSL es fácil de configurar. Esta es una variante de lo que definimos anteriormente:


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. agregue el personalizador de importación que agregará import static java.lang.Math.* a todos los scripts
  2. hacer variable de score disponible para script
  3. ejecutar script

Hay formas de almacenar en caché los scripts en lugar de analizarlos y compilarlos cada vez. Consulte la documentación para más detalles.


Entonces, nuestro script funciona, pero nada impide que el hacker inicie código malicioso. Como planeamos usar la verificación de tipo, recomendaría usar la transformación @CompileStatic :


  • activa la verificación de tipo en el script, y podremos realizar verificaciones adicionales gracias a la extensión para la verificación de tipo
  • mejorar el rendimiento del script

@CompileStatic implícitamente la anotación @CompileStatic a sus scripts es bastante simple. Solo necesita actualizar la configuración del compilador:


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

Ahora, si intentas ejecutar el script nuevamente, verás un error de compilación:


 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 

Que paso Si lee el guión desde el punto de vista del compilador, queda claro que él no sabe nada acerca de la variable "puntaje". Pero usted, como desarrollador, sabe que esta es una variable double , pero el compilador no puede generarla. Para esto, se crean extensiones para la verificación de tipos: puede proporcionar información adicional al compilador, y la compilación funcionará bien. En este caso, debemos indicar que la variable de score es de tipo double .


Por lo tanto, puede cambiar ligeramente la forma en que se @CompileStatic anotación @CompileStatic :


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

Esto "emula" el código anotado por @CompileStatic(extensions=['SecureExtension2.groovy']) . Ahora, por supuesto, necesitamos escribir una extensión que reconozca la variable de score :


SecureExtension2.groovy


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

  1. en caso de que el verificador de tipos no pueda determinar la variable
  2. si el nombre de la variable es score
  3. deje que el compilador defina la variable dinámicamente con el tipo double

Puede encontrar una descripción completa de las extensiones DSL para la verificación de tipos en esta sección de la documentación , pero hay un ejemplo de un modo de compilación combinado: el compilador no puede definir una variable de score . Usted, como desarrollador de DSL, sabe que la variable es en realidad su tipo: double , por lo que la llamada a makeDynamic aquí para decir: "ok, no se preocupe, sé lo que estoy haciendo, esta variable se puede definir dinámicamente con el tipo double " Eso es todo!


Primera extensión "segura" completada


Ahora pongamos todo junto. Escribimos una extensión de verificación de tipo que evita las llamadas a métodos de la clase System por un lado y otra que define la variable de score por el otro. Entonces, si los conectamos, obtenemos la primera extensión completa para la verificación de tipos:


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) } } 

Recuerde actualizar la configuración en su clase Java para usar la nueva extensión para la verificación de tipos:


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

Ejecute el código nuevamente, aún funciona. Ahora intenta esto:


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

La compilación del script se bloqueará con un error:


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

¡Felicitaciones, acaba de escribir la primera extensión de verificación de tipo que evita que se ejecute código malicioso!


Configuración de extensión mejorada


Entonces, todo va bien, podemos prohibir las llamadas a métodos de la clase System , pero parece que pronto se descubrirán nuevas vulnerabilidades y tendremos que evitar el lanzamiento de código malicioso. Entonces, en lugar de codificar todo en la extensión, intentaremos que nuestra extensión sea universal y personalizable. Este es probablemente el más difícil, porque no hay una forma directa de pasar el contexto a la extensión para la verificación de tipos. La idea, por lo tanto, se basa en el uso de una variable local de subproceso (método de curva, sí) para pasar los datos de configuración a los correctores de tipo.


En primer lugar, haremos que la lista de variables sea personalizable. Así se verá el código 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(); } } } 

Conclusión


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


, , . , . , , .


, sandboxing', , — SecureASTCustomizer . , , : secure AST customizer , (, ), ( , ).


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

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


All Articles