Sandboxing aprimorado para scripts interessantes


De um tradutor: ao desenvolver a Plataforma CUBA, colocamos nessa estrutura a capacidade de executar scripts personalizados para uma configuração mais flexível da lógica de negócios do aplicativo. Se essa oportunidade é boa ou ruim (e não estamos falando apenas de CUBA) está sendo debatida há muito tempo, mas o fato de que o controle sobre a execução de scripts de usuário é necessário não levanta nenhuma dúvida. Um dos recursos úteis do Groovy para gerenciar a execução de scripts personalizados é apresentado nesta tradução de Cédric Champeau. Apesar de ele ter deixado recentemente a equipe de desenvolvimento do Groovy, a comunidade de programadores parece estar aproveitando seus trabalhos por um longo tempo.


Uma das maneiras mais usadas de usar o Groovy é por meio de scripts, pois o Groovy facilita a execução dinâmica de código em tempo de execução. Dependendo da aplicação, os scripts podem estar localizados em locais diferentes: o sistema de arquivos, o banco de dados, os serviços remotos ... mas o mais importante é que o desenvolvedor do aplicativo que executa os scripts não os escreve necessariamente. Além disso, os scripts podem funcionar em um ambiente limitado (memória limitada, limite no número de descritores de arquivo, tempo de execução ...), ou você pode impedir o usuário de usar todos os recursos de idioma no script.


Este post irá lhe dizer.


  • por que o groovy é bom para escrever dsl interno
  • quais são seus recursos em termos de segurança do seu aplicativo
  • como configurar a compilação para melhorar o DSL
  • sobre o valor do SecureASTCustomizer
  • sobre extensões de controle de tipo
  • como usar extensões de controle de tipo para tornar o sandbox eficaz

Por exemplo, imagine o que você precisa fazer para que o usuário possa calcular expressões matemáticas. Uma opção de implementação é incorporar uma DSL interna, criar um analisador e, finalmente, um intérprete para essas expressões. Para fazer isso, é claro, você terá que trabalhar, mas se precisar aumentar a produtividade, por exemplo, gerando bytecode para expressões em vez de calculá-las no intérprete ou usar o cache de classes geradas em tempo de execução, o Groovy é uma ótima opção.


Existem muitas opções descritas na documentação , mas o exemplo mais simples é apenas o uso da classe Eval :


Example.java


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

O código 1+1 é analisado, compilado no bytecode, carregado e executado pelo Groovy em tempo de execução. Obviamente, o código neste exemplo é muito simples e você precisará adicionar parâmetros, mas a idéia é que o código executável possa ser arbitrário. E isso pode não ser exatamente o que você precisa. Na calculadora, você precisa permitir algo como isto:


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

mas certamente não


 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! 

É aqui que as dificuldades começam, e também fica claro que precisamos resolver vários problemas:


  • limitar a gramática de um idioma a um subconjunto de seus recursos
  • impedir que os usuários executem o código que não é fornecido
  • impedir a execução de código malicioso

O exemplo da calculadora é bastante simples, mas para DSLs mais complexas, as pessoas podem não perceber que estão escrevendo código problemático, especialmente se o DSL é tão simples que nem os desenvolvedores podem usá- lo .


Alguns anos atrás eu estava nessa situação. Eu desenvolvi um mecanismo que executava "scripts" Groovy escritos por linguistas. Um problema, por exemplo, era que eles poderiam criar inadvertidamente um loop infinito. O código foi executado no servidor e apareceu um encadeamento que consumia 100% da CPU, após o qual foi necessário reiniciar o servidor de aplicativos. Eu tive que procurar uma maneira de resolver o problema sem afetar o DSL, as ferramentas ou o desempenho do aplicativo.


De fato, muitas pessoas têm necessidades semelhantes. Nos últimos 4 anos, conversei com muitas pessoas que tiveram a mesma pergunta: como posso impedir que usuários façam besteiras em scripts Groovy?


Compiladores de personalização


Naquela época, eu já tinha minha própria decisão e sabia que outras pessoas também desenvolviam algo semelhante. No final, Guillaume Laforge sugeriu que eu criasse um mecanismo no kernel do Groovy para ajudar a resolver esses problemas. Ele apareceu no Groovy 1.8.0 como personalizadores de compilação .


Os personalizadores de compilação são um conjunto de classes que modificam o processo de compilação dos scripts do Groovy. Você pode escrever seu próprio personalizador, mas o Groovy fornece:


  • personalizador de importação que adiciona implicitamente importações a scripts para que os usuários não precisem adicionar descrições de importação
  • transformações AST (Abstract Syntax Tree) do personalizador, permitindo adicionar transformações AST diretamente aos scripts
  • Customizador AST seguro que restringe construções gramaticais e de sintaxe de um idioma

O personalizador de transformações AST me ajudou a resolver o problema de loop infinito com a transformação @ThreadInterrupt , mas o SecureASTCustomizer é a coisa que provavelmente é mais mal compreendida na grande maioria dos casos.


Eu deveria me desculpar por isso. Então não consegui encontrar um nome melhor. A parte mais importante no nome “SecureASTCustomizer” é o AST . O objetivo desse mecanismo era limitar o acesso a determinadas funções AST. A palavra "seguro" no título é geralmente supérflua, e vou explicar o porquê. Existe até um post de blog do famoso Kosuke Kawaguchi de Jenkins, intitulado “Fatal Groovy SecureASTCustomizer” . E tudo está escrito muito corretamente lá. O SecureASTCustomizer não foi projetado para sandbox. Foi criado para limitar o idioma no tempo de compilação, mas não na execução. Agora acho que o melhor nome seria GrammarCustomizer . Mas, como você certamente sabe, existem três dificuldades na ciência da computação: invalidação de cache, inventar nomes e um erro por unidade.


Agora imagine que você está considerando o personalizador AST seguro como um meio de garantir a segurança do seu script, e sua tarefa é impedir que o usuário System.exit no script. A documentação diz que as chamadas podem ser barradas em receptores especiais criando listas negras ou brancas. Se for necessária segurança, eu sempre recomendo listas brancas que declarem estritamente o que é permitido, mas não listas negras que proíbem qualquer coisa. Porque os hackers sempre pensam no que você pode não ter considerado. Eu darei um exemplo


Veja como configurar um mecanismo de script de sandbox primitivo usando o SecureASTCustomizer . Embora eu possa escrevê-los no Groovy, dou exemplos de configuração do Java para tornar a diferença entre código de integração e scripts mais 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. criar configuração do compilador
  2. criar personalizador AST seguro
  3. declarar que a classe System como um receptor de chamadas de método está na lista negra
  4. adicionar personalizador à configuração do compilador
  5. vincule a configuração ao script de shell, ou seja, tente criar uma sandbox
  6. execute o script "ruim"
  7. exibir o resultado da execução do script

Se você executar esta classe, ocorrerá um erro durante a execução do 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 conclusão é emitida por um aplicativo com um personalizador AST seguro, que não permite a execução de métodos da classe System . Sucesso! Portanto, protegemos nosso script! Mas espere um minuto ...


O SecureASTCustomizer foi hackeado!


Proteção, diz? Mas e se eu fizer isso:


 def c = System c.exit(-1) 

Se você executar o programa novamente, verá que ele falha sem erro e sem exibir o resultado na tela. O código de saída do processo é -1, o que significa que o script do usuário foi executado! O que aconteceu Em tempo de compilação, o personalizador AST seguro não consegue reconhecer que o c.exit é uma chamada ao método System em princípio porque funciona no nível AST! Ele analisa a chamada do método e, nesse caso, a chamada do método é c.exit(-1) , depois determina o receptor e verifica se está na lista branca (ou preta). Nesse caso, o receptor é c , essa variável é declarada via def e é o mesmo que declarar como um Object , e o personalizador AST seguro pensará que o tipo da variável c é Object , não System !


Em geral, existem muitas maneiras de contornar as várias configurações criadas no personalizador AST seguro. Aqui estão alguns legais:


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

e pode haver muito mais. A natureza dinâmica do Groovy exclui a capacidade de corrigir esses problemas em tempo de compilação. No entanto, existe uma solução. Uma opção é confiar no gerenciador de segurança JVM padrão. No entanto, essa é uma solução pesada e volumosa imediatamente para todo o sistema, e isso é equivalente a disparar um canhão contra pardais. Além disso, ele não funciona em todos os casos, por exemplo, se você deseja proibir a leitura de arquivos, mas não criar ...


Essa limitação - um pouco decepcionante para muitos de nós - levou à criação de uma solução baseada em verificações em tempo de execução . Esse tipo de verificação não tem esses problemas. Por exemplo, porque você saberá o tipo de receptor real da mensagem antes de iniciar a validação da chamada do método. De particular interesse são as seguintes implementações:



No entanto, nenhuma dessas implementações é totalmente confiável e segura. Por exemplo, a versão do Kosuke é baseada no hack da implementação interna do site de chamada em cache. O problema é que ele não é compatível com a versão dinâmica invocada do Groovy e essas classes internas não estarão nas versões futuras do Groovy. A versão de Simon, por outro lado, é baseada em transformações AST, mas deixa muitos buracos em potencial.


Como resultado, meus amigos Corinne Crisch, Fabrice Matrat e Sebastian Blanc, e eu decidimos criar um novo mecanismo de sandbox em tempo de execução, que não terá problemas como esses projetos. Começamos a implementá-lo em uma hackathon em Nice, e na conferência Greach no ano passado, fizemos um relatório sobre isso . Esse mecanismo é baseado em transformações AST e essencialmente reescreve o código para verificar antes de cada chamada de método, tentar acessar o campo de classe, incrementar uma variável, expressão binária ... Essa implementação ainda não está pronta e não há muito trabalho feito, portanto como percebi que o problema com métodos e parâmetros chamados através de "isto implícito" ainda não foi resolvido, como, por exemplo, em construtores:


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

Até o momento, ainda não encontrei uma maneira de resolver esse problema devido à arquitetura do protocolo de meta-objeto no Groovy, que se baseia no fato de que o receptor lança uma exceção quando não consegue encontrar o método antes de alternar para outro receptor. Em resumo, isso significa que você não pode descobrir o tipo de receptor antes da chamada do método real. E se a ligação já passou, é tarde demais ...


E até recentemente, eu não tinha uma solução ideal para esse problema no caso em que o script executável usa as propriedades dinâmicas da linguagem. Mas agora é a hora de explicar como você pode melhorar significativamente a situação se estiver pronto para sacrificar um pouco do dinamismo da linguagem.


Verificação de tipo


Voltemos ao problema principal do SecureASTCustomizer: ele funciona com uma árvore de sintaxe abstrata e não possui informações sobre tipos de mensagens e receptores específicos. Mas com o Groovy 2, o Groovy adicionou compilação e, no Groovy 2.1, adicionamos extensões para verificação de tipo .


Extensões para verificação de tipo são uma coisa muito poderosa: permitem que o desenvolvedor do Groovy DSL ajude o compilador com a inferência de tipo e também permita a geração de erros de compilação nos casos em que geralmente não ocorrem. Essas extensões são usadas internamente pelo Groovy para oferecer suporte a um compilador estático, por exemplo, ao implementar características ou um mecanismo de modelo de marcação .


E se, em vez de usar os resultados do analisador, pudéssemos confiar nas informações do mecanismo de verificação de tipo? Pegue o código que nosso hacker tentou escrever:


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


Se você ativar as verificações de tipo, o código não será compilado:


 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. 

Portanto, esse código não é mais compilado. E se pegarmos este código:


 def c = System c.exit(-1) 

Como você pode ver, ele passa na verificação de tipo, agrupada em um método e executada usando o comando groovy :


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

O verificador de tipos detecta que o método de exit é chamado da classe System e é válido. Isso não vai nos ajudar aqui. Mas o que sabemos é que, se esse código passa na verificação de tipo, significa que o compilador reconhece a chamada para o receptor com o tipo System . Em geral, a idéia é proibir uma chamada com um ramal para verificação de tipo.


Extensão simples para verificação de tipo


Antes de nos aprofundarmos no sandboxing em detalhes, vamos tentar "proteger" nosso script com a ajuda de uma extensão padrão para verificação de tipo. É fácil registrar essa extensão: basta definir o parâmetro extensions para a anotação @TypeChecked (ou @CompileStatic se você estiver usando compilação estática):


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

A pesquisa de extensões ocorrerá no caminho de classe no formato de código-fonte (você pode criar extensões pré-compiladas para verificação de tipo, mas não as consideraremos neste artigo):


SecureExtension1.groovy


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

  1. quando o tipo verificador seleciona um método para chamar
  2. se o método pertencer à classe System
  3. então deixe o verificador de tipos gerar um erro

É tudo o que você precisa. Agora execute o código novamente e você verá um erro de compilação!


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

Desta vez, graças ao tipo verificador, c reconhecido como uma instância da classe System e podemos proibir a chamada. Este é um exemplo muito simples e não demonstra tudo o que pode ser feito com o personalizador AST seguro em termos de configuração. Na extensão que escrevemos , as verificações são codificadas , mas pode ser melhor torná-las personalizáveis. Então, vamos tornar o exemplo mais complicado.


Suponha que seu aplicativo calcule determinadas métricas para um documento e permita que os usuários as personalizem. Nesse caso, DSL:


  • operará (pelo menos) a variável de score
  • permite que os usuários realizem operações matemáticas (incluindo chamar métodos cos , abs , ...)
  • deve proibir todos os outros métodos

Exemplo de script do usuário:


abs(cos(1+score))


Esse DSL é fácil de configurar. Esta é uma variante do que definimos acima:


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. adicione personalizador de importação que adicionará import static java.lang.Math.* a todos os scripts
  2. disponibilizar variável de score para script
  3. executar script

Existem maneiras de armazenar em cache scripts em vez de analisá-los e compilá-los sempre. Veja a documentação para detalhes.


Portanto, nosso script funciona, mas nada impede o hacker de lançar código malicioso. Como planejamos usar a verificação de tipo, eu recomendaria o uso da transformação @CompileStatic :


  • ativa a verificação de tipo no script, e poderemos realizar verificações adicionais graças à extensão para verificação de tipo
  • melhorar o desempenho do script

Adicionar @CompileStatic anotação @CompileStatic aos seus scripts é bastante simples. Você só precisa atualizar a configuração do compilador:


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

Agora, se você tentar executar o script novamente, verá um erro de compilação:


 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 

O que aconteceu Se você ler o script do ponto de vista do compilador, fica claro que ele não sabe nada sobre a variável "score". Mas você, como desenvolvedor, sabe que essa é uma variável double , mas o compilador não pode produzi-la. Para isso, são criadas extensões para verificação de tipo: você pode fornecer informações adicionais ao compilador e a compilação funcionará bem. Nesse caso, precisamos indicar que a variável score é do tipo double .


Portanto, você pode alterar um pouco a maneira @CompileStatic anotação @CompileStatic é @CompileStatic :


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

Isso "emula" o código anotado por @CompileStatic(extensions=['SecureExtension2.groovy']) . Agora, é claro, precisamos escrever uma extensão que reconheça a variável score :


SecureExtension2.groovy


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

  1. caso o verificador de tipos não possa determinar a variável
  2. se o nome da variável for score
  3. deixe o compilador definir a variável dinamicamente com o tipo double

Uma descrição completa das extensões DSL para verificação de tipo pode ser encontrada nesta seção da documentação , mas há um exemplo de um modo de compilação combinado: o compilador não pode definir uma variável de score . Você, como desenvolvedor de DSL, sabe que a variável é realmente o seu tipo - double , então a chamada para makeDynamic aqui para dizer: "ok, não se preocupe, eu sei o que estou fazendo, essa variável pode ser definida dinamicamente com o tipo double " Isso é tudo!


Primeira extensão "segura" concluída


Agora vamos juntar tudo. Escrevemos uma extensão de verificação de tipo que impede chamadas para métodos da classe System por um lado, e outra que define a variável score , por outro. Portanto, se os conectarmos, obteremos a primeira extensão completa para verificação de tipo:


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

Lembre-se de atualizar a configuração em sua classe Java para usar a nova extensão para verificação de tipo:


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

Execute o código novamente - ele ainda funciona. Agora tente o seguinte:


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

A compilação do script falhará com um erro:


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

Parabéns, você acabou de escrever a primeira extensão de verificação de tipo que impede a execução de códigos maliciosos!


Configuração de extensão aprimorada


Portanto, tudo está indo bem, podemos proibir chamadas para métodos da classe System , mas parece que novas vulnerabilidades serão descobertas em breve e precisaremos impedir o lançamento de código malicioso. Então, em vez de codificar tudo na extensão, tentaremos tornar nossa extensão universal e personalizável. Provavelmente, é o mais difícil, porque não há maneira direta de passar o contexto para a extensão para verificação de tipo. A ideia, portanto, é baseada no uso de uma variável local de encadeamento (método de curva, sim) para passar dados de configuração para digitar verificadores.


Primeiro, tornaremos a lista de variáveis ​​personalizável. É assim que o código Java será:


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

Conclusão


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


, , . , . , , .


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


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

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


All Articles