/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.codehaus.groovy.jsr223; import groovy.lang.GroovyClassLoader; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.CodeVisitorSupport; import org.codehaus.groovy.ast.expr.MethodCallExpression; import org.codehaus.groovy.ast.stmt.ExpressionStatement; import org.codehaus.groovy.classgen.GeneratorContext; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilationUnit.PrimaryClassNodeOperation; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.Phases; import org.codehaus.groovy.control.SourceUnit; import org.junit.Before; import org.junit.Test; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.lang.reflect.Field; import java.security.CodeSource; import java.util.HashSet; import java.util.Set; /** * Test for GROOVY-3946 and GROOVY-5255. * * @author Tiago Fernandez */ public class JSR223SecurityTest { class TestFixture { String script = "System.exit 2"; String forbiddenInstruction = "java.lang.System"; } TestFixture testFixture; @Before public void resetTestFixture() { testFixture = new TestFixture(); } @Test(expected = ScriptException.class) public void should_forbid_an_instruction_when_overriding_GroovyClassLoader_using_reflection() throws Exception { secureEval(ClassLoaderDefinitionType.REFLECTION); } @Test(expected = ScriptException.class) public void should_forbid_an_instruction_when_overriding_GroovyClassLoader_using_injection() throws Exception { secureEval(ClassLoaderDefinitionType.INJECTION); } @Test(expected = ScriptException.class) public void should_forbid_an_instruction_when_overriding_GroovyClassLoader_using_constructor() throws Exception { secureEval(ClassLoaderDefinitionType.CONSTRUCTOR); } private void secureEval(ClassLoaderDefinitionType classLoaderDefType) throws Exception { ScriptEngine engine = createScriptEngine(classLoaderDefType); GroovySecurityManager securityMgr = GroovySecurityManager.instance(); securityMgr.overrideGroovyClassLoader(engine, classLoaderDefType); securityMgr.forbid(testFixture.forbiddenInstruction); engine.eval(testFixture.script); } private ScriptEngine createScriptEngine(ClassLoaderDefinitionType classLoaderDefType) { return (classLoaderDefType == ClassLoaderDefinitionType.CONSTRUCTOR) ? new GroovyScriptEngineImpl(new CustomGroovyClassLoader()) : new ScriptEngineManager().getEngineByName("groovy"); } } enum ClassLoaderDefinitionType { CONSTRUCTOR, INJECTION, REFLECTION } class GroovySecurityManager { private static GroovySecurityManager instance = new GroovySecurityManager(); private Set<String> blacklist = new HashSet<String>(); private GroovySecurityManager() { } public synchronized static GroovySecurityManager instance() { return instance; } public void overrideGroovyClassLoader(ScriptEngine engine, ClassLoaderDefinitionType classLoaderDefType) { try { if (classLoaderDefType == ClassLoaderDefinitionType.REFLECTION) { overrideDefaultGroovyClassLoaderUsingReflection(engine); } else if (classLoaderDefType == ClassLoaderDefinitionType.INJECTION) { overrideDefaultGroovyClassLoaderUsingInjection(engine); } } catch (Throwable ex) { throw new RuntimeException("Could not initialize the security manager", ex); } } public void forbid(String instruction) { blacklist.add(instruction); } public boolean isForbidden(String instruction) { for (String forbidden : blacklist) if (instruction.startsWith(forbidden)) return true; return false; } private void overrideDefaultGroovyClassLoaderUsingReflection(ScriptEngine engine) throws Exception { Field classLoader = engine.getClass().getDeclaredField("loader"); classLoader.setAccessible(true); classLoader.set(engine, new CustomGroovyClassLoader()); } private void overrideDefaultGroovyClassLoaderUsingInjection(ScriptEngine engine) throws Exception { GroovyScriptEngineImpl concreteEngine = (GroovyScriptEngineImpl) engine; concreteEngine.setClassLoader(new CustomGroovyClassLoader()); } } class GroovySecurityException extends RuntimeException { public GroovySecurityException(String message) { super(message); } } class CustomGroovyClassLoader extends GroovyClassLoader { protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource source) { CompilationUnit unit = super.createCompilationUnit(config, source); unit.addPhaseOperation(new CustomPrimaryClassNodeOperation(), Phases.SEMANTIC_ANALYSIS); return unit; } } class CustomPrimaryClassNodeOperation extends PrimaryClassNodeOperation { public void call(SourceUnit source, GeneratorContext context, ClassNode classNode) { for (Object statement : source.getAST().getStatementBlock().getStatements()) ((ExpressionStatement) statement).visit(new CustomCodeVisitorSupport()); } } class CustomCodeVisitorSupport extends CodeVisitorSupport { private GroovySecurityManager groovySecurityManager = GroovySecurityManager.instance(); public void visitMethodCallExpression(MethodCallExpression call) { if (groovySecurityManager.isForbidden(call.getText())) throw new GroovySecurityException("The following code is forbidden in the script: " + call.getText()); } }