├── .github ├── CODEOWNERS ├── release-drafter.yml ├── workflows │ └── release-drafter.yml └── dependabot.yml ├── .mvn ├── maven.config └── extensions.xml ├── Jenkinsfile ├── ast.sh ├── src ├── test │ └── java │ │ └── org │ │ └── kohsuke │ │ └── groovy │ │ └── sandbox │ │ ├── no_exit │ │ ├── package-info.java │ │ ├── NoSystemExitSandbox.java │ │ └── NoSystemExitTest.java │ │ ├── robot │ │ ├── package-info.java │ │ ├── Robot.java │ │ ├── RobotSandbox.java │ │ └── RobotTest.java │ │ ├── SomeBean.java │ │ ├── SimpleNamedBean.java │ │ ├── NonArrayConstructorList.java │ │ ├── impl │ │ └── GroovyCallSiteSelectorTest.java │ │ ├── StaticMethodSelectionTest.java │ │ ├── ClassRecorder.java │ │ ├── FinalizerTest.java │ │ └── TheTest.java └── main │ └── java │ └── org │ └── kohsuke │ └── groovy │ └── sandbox │ ├── impl │ ├── Super.java │ ├── ZeroArgInvokerChain.java │ ├── VarArgInvokerChain.java │ ├── TwoArgInvokerChain.java │ ├── SingleArgInvokerChain.java │ ├── SandboxedMethodClosure.java │ ├── InvokerChain.java │ ├── ClosureSupport.java │ ├── Ops.java │ ├── RejectEverythingInterceptor.java │ └── GroovyCallSiteSelector.java │ ├── StackVariableSet.java │ ├── GroovyValueFilter.java │ ├── ScopeTrackingClassCodeExpressionTransformer.java │ ├── GroovyInterceptor.java │ └── SandboxTransformer.java ├── .gitignore ├── LICENSE.md ├── README.md └── pom.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/groovy-sandbox-developers 2 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin(useContainerAgent: true, configurations: [ 2 | [platform: 'linux', jdk: 17], 3 | [platform: 'windows', jdk: 11], 4 | ]) 5 | -------------------------------------------------------------------------------- /ast.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # show the AST tree of the specified Groovy file in GUI 3 | exec groovy -e 'groovy.inspect.swingui.AstBrowser.main(args)' "$@" 4 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/jenkinsci/.github/blob/master/.github/release-drafter.adoc 2 | _extends: .github 3 | tag-template: groovy-sandbox-$NEXT_MINOR_VERSION 4 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/no_exit/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This test demonstrates the canonical "no System.exit() call" situation. 3 | */ 4 | package org.kohsuke.groovy.sandbox.no_exit; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | # mvn hpi:run 4 | work 5 | 6 | # IntelliJ IDEA project files 7 | *.iml 8 | *.iws 9 | *.ipr 10 | .idea 11 | 12 | # Eclipse project files 13 | .settings 14 | .classpath 15 | .project 16 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | update_release_draft: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: release-drafter/release-drafter@v6 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/robot/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This test demonstrates a typical use of the sandboxing, 3 | * where you have a set of objects that are exposed to a sandboxed groovy script for some computation. 4 | * 5 | * The sandboxed script can access those exposed objects, but nothing else. 6 | */ 7 | package org.kohsuke.groovy.sandbox.robot; 8 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | ignore: 10 | # groovy version needs to track Jenkins core, see pom.xml. 11 | - dependency-name: "org.codehaus.groovy:groovy" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/SomeBean.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | /** 4 | * For testing field and attribute access. 5 | * 6 | * @author Kohsuke Kawaguchi 7 | */ 8 | public class SomeBean { 9 | private int x; 10 | 11 | public SomeBean(int x, int y) { 12 | this.x = x; 13 | this.y = y; 14 | } 15 | 16 | int getX() { 17 | return x; 18 | } 19 | 20 | void setX(int x) { 21 | this.x = x; 22 | } 23 | 24 | public int y; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/Super.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor.Invoker; 4 | 5 | /** 6 | * Packs argument of the super method call for {@link Invoker} 7 | * @author Kohsuke Kawaguchi 8 | */ 9 | public final class Super { 10 | final Class senderType; 11 | final Object receiver; 12 | 13 | public Super(Class senderType, Object receiver) { 14 | this.senderType = senderType; 15 | this.receiver = receiver; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitSandbox.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.no_exit; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 4 | 5 | /** 6 | * Reject any static calls to {@link System}. 7 | * 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public class NoSystemExitSandbox extends GroovyInterceptor { 11 | @Override 12 | public Object onStaticCall(GroovyInterceptor.Invoker invoker, Class receiver, String method, Object... args) throws Throwable { 13 | if (receiver == System.class && method.equals("exit")) 14 | throw new SecurityException("No call on System.exit() please"); 15 | return super.onStaticCall(invoker, receiver, method, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/robot/Robot.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.robot; 2 | 3 | /** 4 | * Robot that's exposed to a sandboxed script. 5 | * 6 | * Script can access all aspects of the robot except the brain, which contains a secret. 7 | * 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public class Robot { 11 | public class Arm { 12 | public void wave(int n) { 13 | // wave arms N times 14 | } 15 | } 16 | 17 | public void move() {} 18 | 19 | public class Leg {} 20 | 21 | // scripts will not have access to Brain 22 | public class Brain {} 23 | 24 | public final Brain brain = new Brain(); 25 | 26 | public final Arm leftArm = new Arm(),rightArm = new Arm(); 27 | public final Leg leftLeg = new Leg(),rightLeg = new Leg(); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/ZeroArgInvokerChain.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 4 | 5 | /** 6 | * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. 7 | * 8 | * This version expects no arguments. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | abstract class ZeroArgInvokerChain extends InvokerChain { 13 | protected ZeroArgInvokerChain(Object receiver) { 14 | super(receiver); 15 | } 16 | 17 | public final Object call(Object receiver, String method, Object arg1) throws Throwable { 18 | throw new UnsupportedOperationException(); 19 | } 20 | 21 | public final Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable { 22 | throw new UnsupportedOperationException(); 23 | } 24 | 25 | public final Object call(Object receiver, String method, Object... args) throws Throwable { 26 | if (args.length!=0) 27 | throw new UnsupportedOperationException(); 28 | return call(receiver,method); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/VarArgInvokerChain.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 4 | 5 | /** 6 | * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. 7 | * 8 | * This version is optimized for arbitrary number arguments. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | abstract class VarArgInvokerChain extends InvokerChain { 13 | protected VarArgInvokerChain(Object receiver) { 14 | super(receiver); 15 | } 16 | 17 | public final Object call(Object receiver, String method) throws Throwable { 18 | return call(receiver,method,EMPTY_ARRAY); 19 | } 20 | 21 | public final Object call(Object receiver, String method, Object arg1) throws Throwable { 22 | return call(receiver,method,new Object[]{arg1}); 23 | } 24 | 25 | public final Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable { 26 | return call(receiver,method,new Object[]{arg1,arg2}); 27 | } 28 | 29 | private static final Object[] EMPTY_ARRAY = new Object[0]; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/TwoArgInvokerChain.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 4 | 5 | import java.util.Iterator; 6 | 7 | /** 8 | * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. 9 | * 10 | * This version expects two arguments. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | abstract class TwoArgInvokerChain extends InvokerChain { 15 | protected TwoArgInvokerChain(Object receiver) { 16 | super(receiver); 17 | } 18 | 19 | public final Object call(Object receiver, String method) throws Throwable { 20 | throw new UnsupportedOperationException(); 21 | } 22 | 23 | public final Object call(Object receiver, String method, Object arg1) throws Throwable { 24 | throw new UnsupportedOperationException(); 25 | } 26 | 27 | public final Object call(Object receiver, String method, Object... args) throws Throwable { 28 | if (args.length!=2) 29 | throw new UnsupportedOperationException(); 30 | return call(receiver,method,args[0],args[1]); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/SingleArgInvokerChain.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 4 | 5 | import java.util.Iterator; 6 | 7 | /** 8 | * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. 9 | * 10 | * This version expects exactly one argument. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | abstract class SingleArgInvokerChain extends InvokerChain { 15 | protected SingleArgInvokerChain(Object receiver) { 16 | super(receiver); 17 | } 18 | 19 | public final Object call(Object receiver, String method) throws Throwable { 20 | throw new UnsupportedOperationException(); 21 | } 22 | 23 | public final Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable { 24 | throw new UnsupportedOperationException(); 25 | } 26 | 27 | public final Object call(Object receiver, String method, Object... args) throws Throwable { 28 | if (args.length!=1) 29 | throw new UnsupportedOperationException(); 30 | return call(receiver,method,args[0]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2014 Kohsuke Kawaguchi, CloudBees, Inc., other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | groovy-sandbox 2 | ============== 3 | 4 | **WARNING** This library is only maintained in the context of Jenkins, and should only be used as a dependency of Jenkins plugins such as [Script Security Plugin](https://plugins.jenkins.io/script-security) and [Pipeline: Groovy Plugin](https://plugins.jenkins.io/workflow-cps). It should be considered deprecated and unsafe for all other purposes. 5 | 6 | This library provides a compile-time transformer to run Groovy code in an environment in which most operations, such as method calls, are intercepted before being executed. Consumers of the library can hook into the interception to allow or deny specific operations. 7 | 8 | This library is **not secure** when used by itself. In particular, you must at least use an additional `CompilationCustomizer` along the lines of [RejectASTTransformsCustomizer](https://github.com/jenkinsci/script-security-plugin/blob/c43e099f2f86425b32b0be492020313644062763/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/RejectASTTransformsCustomizer.java) to reject AST transformations that can bypass the sandbox, and you need to take special care to ensure untrusted scripts are both parsed and executed inside of the sandbox. 9 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/robot/RobotSandbox.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.robot; 2 | 3 | import groovy.lang.Closure; 4 | import groovy.lang.Script; 5 | import java.util.Arrays; 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | import org.kohsuke.groovy.sandbox.GroovyValueFilter; 9 | 10 | /** 11 | * This {@link org.kohsuke.groovy.sandbox.GroovyInterceptor} implements a security check. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | public class RobotSandbox extends GroovyValueFilter { 16 | @Override 17 | public Object filter(Object o) { 18 | if (o == null || ALLOWED_TYPES.contains(o.getClass())) 19 | return o; 20 | if (o instanceof Script || o instanceof Closure) 21 | return o; // access to properties of compiled groovy script 22 | throw new SecurityException("Oops, unexpected type: " + o.getClass()); 23 | } 24 | 25 | private static final Set ALLOWED_TYPES = new HashSet<>(Arrays.asList( 26 | Robot.class, 27 | Robot.Arm.class, 28 | Robot.Leg.class, 29 | String.class, 30 | Integer.class, 31 | Boolean.class 32 | // all the primitive types should be OK, but I'm too lazy 33 | 34 | // I'm not adding Class, which rules out all the static method calls 35 | )); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/StackVariableSet.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | /** 7 | * Keep track of in-scope variables on the stack. 8 | * 9 | * In groovy, various statements implicitly create new scopes (as in Java), so we track them 10 | * in a chain. 11 | * 12 | * This only tracks variables on stack (as opposed to field access and closure accessing variables 13 | * in the calling context.) 14 | * 15 | * @author Kohsuke Kawaguchi 16 | */ 17 | final class StackVariableSet implements AutoCloseable { 18 | 19 | final ScopeTrackingClassCodeExpressionTransformer owner; 20 | final StackVariableSet parent; 21 | 22 | private final Set names = new HashSet<>(); 23 | 24 | StackVariableSet(ScopeTrackingClassCodeExpressionTransformer owner) { 25 | this.owner = owner; 26 | this.parent = owner.varScope; 27 | owner.varScope = this; 28 | } 29 | 30 | void declare(String name) { 31 | names.add(name); 32 | } 33 | 34 | /** 35 | * Is the variable of the given name in scope? 36 | */ 37 | boolean has(String name) { 38 | for (StackVariableSet s=this; s!=null; s=s.parent) 39 | if (s.names.contains(name)) 40 | return true; 41 | return false; 42 | } 43 | 44 | @Override 45 | public void close() { 46 | owner.varScope = parent; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/SandboxedMethodClosure.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import groovy.lang.MetaClassImpl; 4 | import org.codehaus.groovy.runtime.InvokerInvocationException; 5 | import org.codehaus.groovy.runtime.MethodClosure; 6 | 7 | import static org.codehaus.groovy.runtime.InvokerHelper.*; 8 | 9 | /** 10 | * {@link MethodClosure} that checks the call. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | public class SandboxedMethodClosure extends MethodClosure { 15 | public SandboxedMethodClosure(Object owner, String method) { 16 | super(owner, method); 17 | } 18 | 19 | /** 20 | * Special logic needed to handle invocation due to not being an instance of MethodClosure itself. See 21 | * {@link MetaClassImpl#invokeMethod(Class, Object, String, Object[], boolean, boolean)} and its special handling 22 | * of {@code objectClass == MethodClosure.class}. 23 | */ 24 | protected Object doCall(Object[] arguments) { 25 | try { 26 | return Checker.checkedCall(getOwner(), false, false, getMethod(), arguments); 27 | } catch (Throwable e) { 28 | throw new InvokerInvocationException(e); 29 | } 30 | } 31 | 32 | protected Object doCall() { 33 | Object[] emptyArgs = {}; 34 | return doCall(emptyArgs); 35 | } 36 | 37 | @Override 38 | protected Object doCall(Object arguments) { 39 | return doCall(asArray(arguments)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/SimpleNamedBean.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2018, CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.kohsuke.groovy.sandbox; 26 | 27 | public class SimpleNamedBean { 28 | private String name; 29 | 30 | public SimpleNamedBean(String n) { 31 | this.name = n; 32 | } 33 | 34 | public String getName() { 35 | return name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitTest.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.no_exit; 2 | 3 | import groovy.lang.GroovyShell; 4 | import junit.framework.TestCase; 5 | import org.codehaus.groovy.control.CompilerConfiguration; 6 | import org.kohsuke.groovy.sandbox.SandboxTransformer; 7 | 8 | /** 9 | * 10 | * 11 | * @author Kohsuke Kawaguchi 12 | */ 13 | public class NoSystemExitTest extends TestCase { 14 | GroovyShell sh; 15 | NoSystemExitSandbox sandbox = new NoSystemExitSandbox(); 16 | 17 | @Override 18 | protected void setUp() { 19 | CompilerConfiguration cc = new CompilerConfiguration(); 20 | cc.addCompilationCustomizers(new SandboxTransformer()); 21 | sh = new GroovyShell(cc); 22 | sandbox.register(); 23 | } 24 | 25 | @Override 26 | protected void tearDown() { 27 | sandbox.unregister(); 28 | } 29 | 30 | void assertFail(String script) { 31 | try { 32 | sh.evaluate(script); 33 | fail("Should have failed"); 34 | } catch (SecurityException e) { 35 | // as expected 36 | } 37 | } 38 | 39 | void eval(String script) { 40 | sh.evaluate(script); 41 | } 42 | 43 | public void test1() { 44 | assertFail("System.exit(-1)"); 45 | assertFail("foo(System.exit(-1))"); 46 | assertFail("System.exit(-1)==System.exit(-1)"); 47 | assertFail("def x=System.&exit; x(-1)"); 48 | 49 | // but this should be OK 50 | eval("System.getProperty('abc')"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/InvokerChain.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 4 | import org.kohsuke.groovy.sandbox.GroovyInterceptor.Invoker; 5 | 6 | import java.util.Collections; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | /** 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | abstract class InvokerChain implements Invoker { 15 | protected final Iterator chain; 16 | 17 | protected InvokerChain(Object receiver) { 18 | // See issue #6, #15. When receiver is null, technically speaking Groovy handles this 19 | // as if NullObject.INSTANCE is the receiver. OTOH, it's confusing 20 | // to GroovyInterceptor that the receiver can be null, so I'm 21 | // bypassing the checker in this case. 22 | if (receiver==null) { 23 | chain = EMPTY_ITERATOR; 24 | } else { 25 | List interceptors = GroovyInterceptor.getApplicableInterceptors(); 26 | if (interceptors.isEmpty()) { 27 | // We are running sandbox-transformed code, but there is no interceptor on the current thread. 28 | // This is dangerous (SECURITY-2020), so we reject everything. 29 | chain = REJECT_EVERYTHING.iterator(); 30 | } else { 31 | chain = interceptors.iterator(); 32 | } 33 | } 34 | } 35 | 36 | private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator(); 37 | private static final Set REJECT_EVERYTHING = Collections.singleton(new RejectEverythingInterceptor()); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/NonArrayConstructorList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2018, CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.kohsuke.groovy.sandbox; 26 | 27 | 28 | import java.util.ArrayList; 29 | 30 | /** 31 | * Used in {@link TheTest#testCheckedCastWhenAssignable()} - couldn't be an inner class due to gmaven issues. 32 | */ 33 | public class NonArrayConstructorList extends ArrayList { 34 | public NonArrayConstructorList(boolean choiceOne, boolean choiceTwo) { 35 | if (choiceOne) { 36 | this.add("one"); 37 | } 38 | if (choiceTwo) { 39 | this.add("two"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelectorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2020 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.kohsuke.groovy.sandbox.impl; 26 | 27 | import org.junit.Test; 28 | 29 | import static org.hamcrest.CoreMatchers.equalTo; 30 | import static org.junit.Assert.assertThat; 31 | import static org.junit.Assert.fail; 32 | 33 | public class GroovyCallSiteSelectorTest { 34 | 35 | @Test public void missingConstructor() { 36 | try { 37 | GroovyCallSiteSelector.findConstructor(GroovyCallSiteSelectorTest.class, new Object[]{ 1, 'a' }, null); 38 | fail("Constructor should not have been found"); 39 | } catch (SecurityException e) { 40 | assertThat(e.getMessage(), equalTo("Unable to find constructor: new org.kohsuke.groovy.sandbox.impl.GroovyCallSiteSelectorTest java.lang.Integer java.lang.Character")); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/robot/RobotTest.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.robot; 2 | 3 | import groovy.lang.Binding; 4 | import groovy.lang.GroovyShell; 5 | import junit.framework.TestCase; 6 | import org.codehaus.groovy.control.CompilerConfiguration; 7 | import org.kohsuke.groovy.sandbox.SandboxTransformer; 8 | 9 | /** 10 | * 11 | * 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | public class RobotTest extends TestCase { 15 | Robot robot; 16 | GroovyShell sh; 17 | RobotSandbox sandbox = new RobotSandbox(); 18 | 19 | @Override 20 | protected void setUp() { 21 | CompilerConfiguration cc = new CompilerConfiguration(); 22 | cc.addCompilationCustomizers(new SandboxTransformer()); 23 | Binding binding = new Binding(); 24 | binding.setProperty("robot", robot = new Robot()); 25 | sh = new GroovyShell(binding,cc); 26 | sandbox.register(); 27 | } 28 | 29 | @Override 30 | protected void tearDown() { 31 | sandbox.unregister(); 32 | } 33 | 34 | void assertFail(String script) { 35 | try { 36 | sh.evaluate(script); 37 | fail("Should have failed"); 38 | } catch (SecurityException e) { 39 | // as expected 40 | } 41 | } 42 | 43 | void eval(String script) { 44 | sh.evaluate(script); 45 | } 46 | 47 | public void test1() { 48 | // these are OK 49 | eval("robot.leftArm.wave(3)"); 50 | eval("[robot.@leftArm,robot.@rightArm]*.wave(3)"); 51 | eval("if (robot.leftArm!=null) robot.leftArm.wave(1)"); 52 | eval("def c = { x -> x.leftArm.wave(3) }; c(robot);"); 53 | 54 | // these are not 55 | assertFail("robot.brain"); 56 | assertFail("robot.@brain"); 57 | assertFail("robot['brain']"); 58 | assertFail("System.exit(-1)"); 59 | assertFail("def c = { -> delegate = System; exit(-1) }; c();"); 60 | assertFail("Class.forName('java.lang.String')"); 61 | assertFail("'foo'.class.name"); 62 | assertFail("new java.awt.Point(1,2)"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 4.86 9 | 10 | 11 | 12 | org.kohsuke 13 | groovy-sandbox 14 | ${revision}${changelist} 15 | https://github.com/jenkinsci/${project.artifactId} 16 | 17 | Groovy Sandbox 18 | Executes untrusted Groovy script safely 19 | 20 | 21 | 1.35 22 | -SNAPSHOT 23 | jenkinsci/${project.artifactId} 24 | 25 | false 26 | 27 | 28 | 29 | 30 | repo.jenkins-ci.org 31 | https://repo.jenkins-ci.org/public/ 32 | 33 | 34 | 35 | 36 | 37 | repo.jenkins-ci.org 38 | https://repo.jenkins-ci.org/public/ 39 | 40 | 41 | 42 | 43 | 44 | org.codehaus.groovy 45 | groovy 46 | 2.4.21 47 | 48 | 49 | 50 | 51 | scm:git:https://github.com/${gitHubRepo}.git 52 | scm:git:git@github.com:${gitHubRepo}.git 53 | https://github.com/${gitHubRepo} 54 | ${scmTag} 55 | 56 | 57 | 58 | 59 | MIT License 60 | https://opensource.org/licenses/MIT 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/ClosureSupport.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import groovy.lang.Closure; 4 | 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | /** 12 | * Helps with sanbox intercepting Closures, which has unique dispatching rules we need to understand. 13 | * 14 | * @author Kohsuke Kawaguchi 15 | */ 16 | final class ClosureSupport { 17 | /** 18 | * {@link Closure} forwards methods/properties to other objects, depending on the resolution strategy. 19 | *

20 | * This method returns the list of non-null objects that should be considered, in that order. 21 | */ 22 | public static List targetsOf(Closure receiver) { 23 | Object owner = receiver.getOwner(); 24 | Object delegate = receiver.getDelegate(); 25 | 26 | // Groovy's method dispatch logic for Closure is defined in MetaClassImpl.invokeMethod 27 | switch (receiver.getResolveStrategy()) { 28 | case Closure.OWNER_FIRST: 29 | return of(owner,delegate); 30 | case Closure.DELEGATE_FIRST: 31 | return of(delegate,owner); 32 | case Closure.OWNER_ONLY: 33 | return of(owner); 34 | case Closure.DELEGATE_ONLY: 35 | return of(delegate); 36 | case Closure.TO_SELF: 37 | default: 38 | // fields/methods defined on Closure are checked by SandboxInterceptor, 39 | // so if we are here it means we will not find the target of the dispatch. 40 | return Collections.emptyList(); 41 | } 42 | } 43 | 44 | private static List of(Object o1, Object o2) { 45 | // various cases where the list of two become the list of one (or empty) 46 | if (o1==null) return of(o2); 47 | if (o2==null) return of(o1); 48 | if (o1==o2) return of(o1); 49 | 50 | return Arrays.asList(o1, o2); 51 | } 52 | 53 | private static List of(Object maybeNull) { 54 | if (maybeNull==null) 55 | return Collections.emptyList(); 56 | return Collections.singletonList(maybeNull); 57 | } 58 | 59 | /** 60 | * Built-in properties on {@link Closure} that do not follow the delegation rules. 61 | */ 62 | public static final Set BUILTIN_PROPERTIES = new HashSet(Arrays.asList( 63 | "delegate", 64 | "owner", 65 | "maximumNumberOfParameters", 66 | "parameterTypes", 67 | "metaClass", 68 | "class", 69 | "directive", 70 | "resolveStrategy", 71 | "thisObject" 72 | )); 73 | 74 | } -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/StaticMethodSelectionTest.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import groovy.lang.GroovyShell; 4 | import static org.junit.Assert.fail; 5 | import org.junit.Test; 6 | 7 | /** 8 | * 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public class StaticMethodSelectionTest { 13 | 14 | public static void strangeThirdSelection(Class x, Class y) { 15 | fail("I'm expecting this method not to be invoked"); 16 | } 17 | 18 | /* 19 | A part of call routing to onMethodCall vs onStaticCall requires that we emulate the groovy's method 20 | picking logic. This is implemented inside Groovy in MetaClassImpl.chooseMethod. 21 | 22 | In 1.8.5, The first call to chooseMethod picks a static method defined on the class, 23 | then the 2nd check looks for instance methods from java.lang.Class. 24 | 25 | But the third one is strange, as it's checking the static methods defined on this class again, 26 | but with extra MetaClassHelper.convertToTypeArray(arguments)). Since arguments is already Class[], 27 | this means it will find a method like static void Foo.foo(Class,Class) against a call 28 | like Foo.foo(1,2). When I tried this in a test, the call subsequently fail with 29 | java.lang.reflect.Method.invoke(): 30 | 31 | This is most likely a bug in Groovy, but since I cannot be certain, writing a test case here 32 | to monitor the behaviour change. 33 | 34 | private MetaMethod pickStaticMethod(String methodName, Class[] arguments) { 35 | MetaMethod method = null; 36 | MethodSelectionException mse = null; 37 | Object methods = getStaticMethods(theClass, methodName); 38 | 39 | if (!(methods instanceof FastArray) || !((FastArray)methods).isEmpty()) { 40 | try { 41 | method = (MetaMethod) chooseMethod(methodName, methods, arguments); 42 | } catch(MethodSelectionException msex) { 43 | mse = msex; 44 | } 45 | } 46 | if (method == null && theClass != Class.class) { 47 | MetaClass classMetaClass = registry.getMetaClass(Class.class); 48 | method = classMetaClass.pickMethod(methodName, arguments); 49 | } 50 | if (method == null) { 51 | method = (MetaMethod) chooseMethod(methodName, methods, MetaClassHelper.convertToTypeArray(arguments)); 52 | } 53 | 54 | if (method == null && mse != null) { 55 | throw mse; 56 | } else { 57 | return method; 58 | } 59 | } 60 | 61 | */ 62 | @Test 63 | public void testStrangeThirdSelection() { 64 | try { 65 | new GroovyShell().evaluate("org.kohsuke.groovy.sandbox.StaticMethodSelectionTest.strangeThirdSelection(1, 2)"); 66 | fail(); 67 | } catch (IllegalArgumentException e) { 68 | assert e.getMessage().contains("argument type mismatch"); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/Ops.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox.impl; 2 | 3 | import org.codehaus.groovy.syntax.Types; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static org.codehaus.groovy.syntax.Types.*; 9 | 10 | /** 11 | * Additional relationship between operators. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | * @see Types 15 | */ 16 | public class Ops { 17 | private static final Map compoundAssignmentToBinaryOperator = new HashMap(); 18 | 19 | public static int compoundAssignmentToBinaryOperator(int type) { 20 | Integer o = compoundAssignmentToBinaryOperator.get(type); 21 | if (o==null) throw new IllegalArgumentException(""+type); 22 | return o; 23 | } 24 | 25 | private static final Map binaryOperatorMethods = new HashMap(); 26 | 27 | public static String binaryOperatorMethods(int type) { 28 | String v = binaryOperatorMethods.get(type); 29 | if (v==null) throw new IllegalArgumentException(""+type); 30 | return v; 31 | } 32 | 33 | public static boolean isComparisionOperator(int type) { 34 | return Types.ofType(type,COMPARISON_OPERATOR); 35 | } 36 | 37 | public static boolean isRegexpComparisonOperator(int type) { 38 | return Types.ofType(type,REGEX_COMPARISON_OPERATOR); 39 | } 40 | 41 | public static boolean isLogicalOperator(int type) { 42 | return Types.ofType(type,LOGICAL_OPERATOR); 43 | } 44 | 45 | 46 | // see http://groovy.codehaus.org/Operator+Overloading 47 | static { 48 | Map c = compoundAssignmentToBinaryOperator; 49 | c.put(PLUS_EQUAL,PLUS); 50 | c.put(MINUS_EQUAL,MINUS); 51 | c.put(MULTIPLY_EQUAL,MULTIPLY); 52 | c.put(DIVIDE_EQUAL,DIVIDE); 53 | c.put(INTDIV_EQUAL,INTDIV); 54 | c.put(MOD_EQUAL,MOD); 55 | c.put(POWER_EQUAL,POWER); 56 | 57 | c.put(LEFT_SHIFT_EQUAL, LEFT_SHIFT); 58 | c.put(RIGHT_SHIFT_EQUAL, RIGHT_SHIFT); 59 | c.put(RIGHT_SHIFT_UNSIGNED_EQUAL, RIGHT_SHIFT_UNSIGNED); 60 | 61 | c.put(BITWISE_OR_EQUAL, BITWISE_OR); 62 | c.put(BITWISE_AND_EQUAL, BITWISE_AND); 63 | c.put(BITWISE_XOR_EQUAL, BITWISE_XOR); 64 | 65 | // see BinaryExpressionHelper.eval 66 | Map b = binaryOperatorMethods; 67 | b.put(PLUS,"plus"); 68 | b.put(MINUS,"minus"); 69 | b.put(MULTIPLY,"multiply"); 70 | b.put(POWER,"power"); 71 | b.put(DIVIDE,"div"); 72 | b.put(MOD,"mod"); 73 | b.put(BITWISE_OR,"or"); 74 | b.put(BITWISE_AND,"and"); 75 | b.put(BITWISE_XOR,"xor"); 76 | b.put(LEFT_SHIFT,"leftShift"); 77 | b.put(RIGHT_SHIFT,"rightShift"); 78 | b.put(RIGHT_SHIFT_UNSIGNED,"rightShiftUnsigned"); 79 | 80 | b.put(COMPARE_EQUAL,"compareEqual"); 81 | b.put(COMPARE_NOT_EQUAL,"compareNotEqual"); 82 | b.put(COMPARE_LESS_THAN,"compareLessThan"); 83 | b.put(COMPARE_LESS_THAN_EQUAL,"compareLessThanEqual"); 84 | b.put(COMPARE_GREATER_THAN,"compareGreaterThan"); 85 | b.put(COMPARE_GREATER_THAN_EQUAL,"compareGreaterThanEqual"); 86 | b.put(COMPARE_TO,"compareTo"); 87 | 88 | b.put(FIND_REGEX,"findRegex"); 89 | b.put(MATCH_REGEX,"matchRegex"); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/GroovyValueFilter.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import groovy.lang.Binding; 4 | import groovy.lang.Script; 5 | 6 | /** 7 | * @deprecated 8 | */ 9 | @Deprecated 10 | public class GroovyValueFilter extends GroovyInterceptor { 11 | /** 12 | * Called for every receiver. 13 | */ 14 | public Object filterReceiver(Object receiver) { 15 | return filter(receiver); 16 | } 17 | 18 | /** 19 | * Called for a return value of a method call, newly created object, retrieve property/attribute values. 20 | */ 21 | public Object filterReturnValue(Object returnValue) { 22 | return filter(returnValue); 23 | } 24 | 25 | /** 26 | * Called for every argument to method/constructor calls. 27 | */ 28 | public Object filterArgument(Object arg) { 29 | return filter(arg); 30 | } 31 | 32 | /** 33 | * Called for every index of the array get/set access. 34 | */ 35 | public Object filterIndex(Object index) { 36 | return filter(index); 37 | } 38 | 39 | /** 40 | * All the specific {@code filterXXX()} methods delegate to this method. 41 | */ 42 | public Object filter(Object o) { 43 | return o; 44 | } 45 | 46 | private Object[] filterArguments(Object[] args) { 47 | for (int i=0; i0) b.append(','); 39 | b.append(type(o)); 40 | } 41 | return b.toString(); 42 | } 43 | 44 | @Override 45 | public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable { 46 | format("%s.%s(%s)",type(receiver),method,arguments(args)); 47 | return super.onMethodCall(invoker, receiver, method, args); 48 | } 49 | 50 | @Override 51 | public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable { 52 | format("%s:%s(%s)",type(receiver),method,arguments(args)); 53 | return super.onStaticCall(invoker, receiver, method, args); 54 | } 55 | 56 | @Override 57 | public Object onNewInstance(Invoker invoker, Class receiver, Object... args) throws Throwable { 58 | format("new %s(%s)",type(receiver),arguments(args)); 59 | return super.onNewInstance(invoker, receiver, args); 60 | } 61 | 62 | @Override 63 | public Object onSuperCall(Invoker invoker, Class senderType, Object receiver, String method, Object... args) throws Throwable { 64 | format("%s.super(%s).%s(%s)",type(receiver),type(senderType),method,arguments(args)); 65 | return super.onSuperCall(invoker, senderType, receiver, method, args); 66 | } 67 | 68 | @Override 69 | public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable { 70 | format("%s.%s",type(receiver),property); 71 | return super.onGetProperty(invoker, receiver, property); 72 | } 73 | 74 | @Override 75 | public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable { 76 | format("%s.%s=%s",type(receiver),property,type(value)); 77 | return super.onSetProperty(invoker, receiver, property, value); 78 | } 79 | 80 | @Override 81 | public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable { 82 | format("%s.@%s",type(receiver),attribute); 83 | return super.onGetAttribute(invoker, receiver, attribute); 84 | } 85 | 86 | @Override 87 | public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable { 88 | format("%s.@%s=%s",type(receiver),attribute,type(value)); 89 | return super.onSetAttribute(invoker, receiver, attribute, value); 90 | } 91 | 92 | @Override 93 | public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable { 94 | format("%s[%s]",type(receiver),type(index)); 95 | return super.onGetArray(invoker, receiver, index); 96 | } 97 | 98 | @Override 99 | public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable { 100 | format("%s[%s]=%s",type(receiver),type(index),type(value)); 101 | return super.onSetArray(invoker, receiver, index, value); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/RejectEverythingInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2020 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.kohsuke.groovy.sandbox.impl; 26 | 27 | import java.util.stream.Collectors; 28 | import java.util.stream.Stream; 29 | import org.kohsuke.groovy.sandbox.GroovyInterceptor; 30 | import org.kohsuke.groovy.sandbox.GroovyInterceptor.Invoker; 31 | /** 32 | * An interceptor used by {@link Invoker} to reject any sandbox-transformed code that is executed when 33 | * {@link GroovyInterceptor#getApplicableInterceptors} is empty, under the assumption that there is no legitimate 34 | * reason to run sandbox-transformed code outside of the sandbox.

35 | * 36 | * Parameters of overridden methods with type {@link Object} are assumed to be unsafe and must be handled carefully to 37 | * avoid security vulnerabilities. Safe operations include casting these objects to known-safe final classes such as 38 | * {@link String}, or calling known-safe final methods such as {@link Object#getClass}. 39 | */ 40 | public class RejectEverythingInterceptor extends GroovyInterceptor { 41 | 42 | @Override 43 | public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable { 44 | throw new SecurityException("Rejecting unsandboxed method call: " + getClassName(receiver) + "." + method + getArgumentClassNames(args)); 45 | } 46 | 47 | @Override 48 | public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable { 49 | throw new SecurityException("Rejecting unsandboxed static method call: " + getClassName(receiver) + "." + method + getArgumentClassNames(args)); 50 | } 51 | 52 | @Override 53 | public Object onNewInstance(Invoker invoker, Class receiver, Object... args) throws Throwable { 54 | throw new SecurityException("Rejecting unsandboxed constructor call: " + getClassName(receiver) + getArgumentClassNames(args)); 55 | } 56 | 57 | @Override 58 | public Object onSuperCall(Invoker invoker, Class senderType, Object receiver, String method, Object... args) throws Throwable { 59 | throw new SecurityException("Rejecting unsandboxed super method call: " + getClassName(receiver) + "." + method + getArgumentClassNames(args)); 60 | } 61 | 62 | @Override 63 | public void onSuperConstructor(Invoker invoker, Class receiver, Object... args) throws Throwable { 64 | throw new SecurityException("Rejecting unsandboxed super constructor call: " + getClassName(receiver) + getArgumentClassNames(args)); 65 | } 66 | 67 | @Override 68 | public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable { 69 | throw new SecurityException("Rejecting unsandboxed property get: " + getClassName(receiver) + "." + property); 70 | } 71 | 72 | @Override 73 | public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable { 74 | throw new SecurityException("Rejecting unsandboxed property set: " + getClassName(receiver) + "." + property + " = " + getClassName(value)); 75 | } 76 | 77 | @Override 78 | public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable { 79 | throw new SecurityException("Rejecting unsandboxed attribute get: " + getClassName(receiver) + "." + attribute); 80 | } 81 | 82 | @Override 83 | public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable { 84 | throw new SecurityException("Rejecting unsandboxed attribute set: " + getClassName(receiver) + "." + attribute + " = " + getClassName(value)); 85 | } 86 | 87 | @Override 88 | public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable { 89 | throw new SecurityException("Rejecting unsandboxed array get: " + getClassName(receiver) + "[" + getArrayIndex(index) + "]"); 90 | } 91 | 92 | @Override 93 | public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable { 94 | throw new SecurityException("Rejecting unsandboxed array set: " + getClassName(receiver) + "[" + getArrayIndex(index) + "] = " + getClassName(value)); 95 | } 96 | 97 | private static String getClassName(Object value) { 98 | if (value == null) { 99 | return null; 100 | } else if (value instanceof Class) { 101 | return ((Class) value).getName(); 102 | } else { 103 | return value.getClass().getName(); 104 | } 105 | } 106 | 107 | private static String getArgumentClassNames(Object[] args) { 108 | return Stream.of(args) 109 | .map(RejectEverythingInterceptor::getClassName) 110 | .collect(Collectors.joining(", ", "(", ")")); 111 | } 112 | 113 | private static String getArrayIndex(Object value) { 114 | if (value == null) { 115 | return "null"; 116 | } else if (value instanceof Integer) { 117 | return value.toString(); 118 | } else { 119 | return value.getClass().getName(); 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2020 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.kohsuke.groovy.sandbox.impl; 26 | 27 | import java.lang.reflect.Constructor; 28 | import java.util.Map; 29 | import java.util.stream.Collectors; 30 | import java.util.stream.Stream; 31 | import org.codehaus.groovy.reflection.ParameterTypes; 32 | import org.codehaus.groovy.runtime.MetaClassHelper; 33 | 34 | public class GroovyCallSiteSelector { 35 | 36 | private GroovyCallSiteSelector() {} 37 | 38 | /** 39 | * Find the {@link Constructor} that Groovy will invoke at runtime for the given type and arguments. 40 | * 41 | * @throws SecurityException if no valid constructor is found, or if the constructor is a synthetic constructor 42 | * added by SandboxTransformer and the constructor wrapper argument is invalid. 43 | */ 44 | public static Constructor findConstructor(Class type, Object[] args, Class expectedConstructorWrapper) { 45 | Constructor c = constructor(type, args); 46 | if (c == null) { 47 | throw new SecurityException("Unable to find constructor: " + GroovyCallSiteSelector.formatConstructor(type, args)); 48 | } 49 | // Check to make sure that users are not directly calling synthetic constructors without going through 50 | // `Checker.checkedSuperConstructor` or `Checker.checkedThisConstructor`. Part of SECURITY-1754. 51 | if (isSandboxGeneratedConstructor(c) && ( 52 | expectedConstructorWrapper == null || // Generated constructors should never be called directly, so any call from Checker.checkedConstructor should be rejected 53 | args.length < 1 || // Should always be false since isSandboxGeneratedConstructor returned true 54 | args[0] == null || // The wrapper argument must not be null 55 | args[0].getClass() != expectedConstructorWrapper)) { // The first argument must match the expected wrapper type 56 | String alternateConstructors = Stream.of(c.getDeclaringClass().getDeclaredConstructors()) 57 | .filter(tempC -> !isSandboxGeneratedConstructor(tempC)) 58 | .map(Object::toString) 59 | .sorted() 60 | .collect(Collectors.joining(", ")); 61 | throw new SecurityException("Rejecting illegal call to synthetic constructor: " + c + ". Perhaps you meant to use one of these constructors instead: " + alternateConstructors); 62 | } 63 | return c; 64 | } 65 | 66 | static Constructor constructor(Class receiver, Object[] args) { 67 | Constructor[] constructors = receiver.getDeclaredConstructors(); 68 | Constructor bestMatch = null; 69 | ParameterTypes bestMatchParamTypes = null; 70 | Class[] argTypes = MetaClassHelper.convertToTypeArray(args); 71 | for (Constructor c : constructors) { 72 | ParameterTypes cParamTypes = new ParameterTypes(c.getParameterTypes()); 73 | if (cParamTypes.isValidMethod(argTypes)) { 74 | if (bestMatch == null || isMoreSpecific(cParamTypes, bestMatchParamTypes, argTypes)) { 75 | bestMatch = c; 76 | bestMatchParamTypes = cParamTypes; 77 | } 78 | } 79 | } 80 | if (bestMatch != null) { 81 | return bestMatch; 82 | } 83 | 84 | // Only check for the magic Map constructor if we haven't already found a real constructor. 85 | // Also note that this logic is derived from how Groovy itself decides to use the magic Map constructor, at 86 | // MetaClassImpl#invokeConstructor(Class, Object[]). 87 | if (args.length == 1 && args[0] instanceof Map) { 88 | for (Constructor c : constructors) { 89 | if (c.getParameterTypes().length == 0 && !c.isVarArgs()) { 90 | return c; 91 | } 92 | } 93 | } 94 | 95 | return null; 96 | } 97 | 98 | public static boolean isMoreSpecific(ParameterTypes paramsForCandidate, ParameterTypes paramsForBaseline, Class[] argTypes) { 99 | long candidateDistance = MetaClassHelper.calculateParameterDistance(argTypes, paramsForCandidate); 100 | long currentBestDistance = MetaClassHelper.calculateParameterDistance(argTypes, paramsForBaseline); 101 | return candidateDistance < currentBestDistance; 102 | } 103 | 104 | private static final Class[] SYNTHETIC_CONSTRUCTOR_PARAMETER_TYPES = new Class[] { 105 | Checker.SuperConstructorWrapper.class, 106 | Checker.ThisConstructorWrapper.class, 107 | }; 108 | 109 | /** 110 | * @return true if this constructor is one that was added by groovy-sandbox in {@code SandboxTransformer.processConstructors} 111 | * specifically to be able to intercept calls to super in constructors. 112 | */ 113 | private static boolean isSandboxGeneratedConstructor(Constructor c) { 114 | if (!c.isSynthetic()) { 115 | return false; 116 | } 117 | Class[] parameterTypes = c.getParameterTypes(); 118 | if (parameterTypes.length > 0) { 119 | for (Class syntheticParamType : SYNTHETIC_CONSTRUCTOR_PARAMETER_TYPES) { 120 | if (parameterTypes[0] == syntheticParamType) { 121 | return true; 122 | } 123 | } 124 | } 125 | return false; 126 | } 127 | 128 | public static String formatConstructor(Class c, Object... args) { 129 | return "new " + getName(c) + printArgumentTypes(args); 130 | } 131 | 132 | private static String printArgumentTypes(Object[] args) { 133 | StringBuilder b = new StringBuilder(); 134 | for (Object arg : args) { 135 | b.append(' '); 136 | b.append(getName(arg)); 137 | } 138 | return b.toString(); 139 | } 140 | 141 | public static String getName(Object o) { 142 | return o == null ? "null" : getName(o.getClass()); 143 | } 144 | 145 | private static String getName(Class c) { 146 | Class e = c.getComponentType(); 147 | if (e == null) { 148 | return c.getName(); 149 | } else { 150 | return getName(e) + "[]"; 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/ScopeTrackingClassCodeExpressionTransformer.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import org.codehaus.groovy.ast.ClassCodeExpressionTransformer; 4 | import org.codehaus.groovy.ast.FieldNode; 5 | import org.codehaus.groovy.ast.MethodNode; 6 | import org.codehaus.groovy.ast.Parameter; 7 | import org.codehaus.groovy.ast.Variable; 8 | import org.codehaus.groovy.ast.expr.BooleanExpression; 9 | import org.codehaus.groovy.ast.expr.DeclarationExpression; 10 | import org.codehaus.groovy.ast.expr.Expression; 11 | import org.codehaus.groovy.ast.expr.TupleExpression; 12 | import org.codehaus.groovy.ast.expr.VariableExpression; 13 | import org.codehaus.groovy.ast.stmt.BlockStatement; 14 | import org.codehaus.groovy.ast.stmt.CatchStatement; 15 | import org.codehaus.groovy.ast.stmt.DoWhileStatement; 16 | import org.codehaus.groovy.ast.stmt.ForStatement; 17 | import org.codehaus.groovy.ast.stmt.IfStatement; 18 | import org.codehaus.groovy.ast.stmt.SwitchStatement; 19 | import org.codehaus.groovy.ast.stmt.SynchronizedStatement; 20 | import org.codehaus.groovy.ast.stmt.TryCatchStatement; 21 | import org.codehaus.groovy.ast.stmt.WhileStatement; 22 | 23 | /** 24 | * Keeps track of in-scope variables. 25 | * 26 | * @author Kohsuke Kawaguchi 27 | */ 28 | abstract class ScopeTrackingClassCodeExpressionTransformer extends ClassCodeExpressionTransformer { 29 | /** 30 | * As we visit expressions, track variable scopes. 31 | * This is used to distinguish local variables from property access. See issue #11. 32 | */ 33 | StackVariableSet varScope; 34 | 35 | public boolean isLocalVariable(String name) { 36 | return varScope.has(name); 37 | } 38 | 39 | @Override 40 | public void visitMethod(MethodNode node) { 41 | varScope = null; 42 | try (StackVariableSet scope = new StackVariableSet(this)) { 43 | for (Parameter p : node.getParameters()) { 44 | declareVariable(p); 45 | } 46 | super.visitMethod(node); 47 | } 48 | } 49 | 50 | void withMethod(MethodNode node, Runnable r) { 51 | varScope = null; 52 | try (StackVariableSet scope = new StackVariableSet(this)) { 53 | for (Parameter p : node.getParameters()) { 54 | declareVariable(p); 55 | } 56 | r.run(); 57 | } 58 | } 59 | 60 | @Override 61 | public void visitField(FieldNode node) { 62 | try (StackVariableSet scope = new StackVariableSet(this)) { 63 | super.visitField(node); 64 | } 65 | } 66 | 67 | @Override 68 | public void visitBlockStatement(BlockStatement block) { 69 | try (StackVariableSet scope = new StackVariableSet(this)) { 70 | super.visitBlockStatement(block); 71 | } 72 | } 73 | 74 | @Override 75 | public void visitDoWhileLoop(DoWhileStatement loop) { 76 | // Do-while loops are not actually supported by Groovy 2.x. 77 | try (StackVariableSet scope = new StackVariableSet(this)) { 78 | loop.getLoopBlock().visit(this); 79 | } 80 | try (StackVariableSet scope = new StackVariableSet(this)) { 81 | loop.setBooleanExpression((BooleanExpression) transform(loop.getBooleanExpression())); 82 | } 83 | } 84 | 85 | @Override 86 | public void visitForLoop(ForStatement forLoop) { 87 | try (StackVariableSet scope = new StackVariableSet(this)) { 88 | /* 89 | Groovy appears to always treat the left-hand side of forLoop as a declaration. 90 | i.e., the following code is error 91 | 92 | def h() { 93 | def x =0; 94 | def i = 0; 95 | for (i in 0..9 ) { 96 | x+= i; 97 | } 98 | println x; 99 | } 100 | 101 | script1414457812466.groovy: 18: The current scope already contains a variable of the name i 102 | @ line 18, column 5. 103 | for (i in 0..9 ) { 104 | ^ 105 | 106 | 1 error 107 | 108 | Also see issue 17. 109 | */ 110 | if (!ForStatement.FOR_LOOP_DUMMY.equals(forLoop.getVariable())) { 111 | // When using Java-style for loops, the 3 expressions are a ClosureListExpression and ForStatement.getVariable is a dummy value that we need to ignore. 112 | declareVariable(forLoop.getVariable()); 113 | } 114 | // Avoid super.visitForLoop because it transforms the collection expression but then recurses on the entire 115 | // ForStatement, causing the collection expression to be visited a second time. 116 | forLoop.setCollectionExpression(transform(forLoop.getCollectionExpression())); 117 | forLoop.getLoopBlock().visit(this); 118 | } 119 | } 120 | 121 | @Override 122 | public void visitIfElse(IfStatement ifElse) { 123 | try (StackVariableSet scope = new StackVariableSet(this)) { 124 | ifElse.setBooleanExpression((BooleanExpression)transform(ifElse.getBooleanExpression())); 125 | } 126 | try (StackVariableSet scope = new StackVariableSet(this)) { 127 | ifElse.getIfBlock().visit(this); 128 | } 129 | try (StackVariableSet scope = new StackVariableSet(this)) { 130 | ifElse.getElseBlock().visit(this); 131 | } 132 | } 133 | 134 | @Override 135 | public void visitSwitch(SwitchStatement statement) { 136 | try (StackVariableSet scope = new StackVariableSet(this)) { 137 | super.visitSwitch(statement); 138 | } 139 | } 140 | 141 | @Override 142 | public void visitSynchronizedStatement(SynchronizedStatement sync) { 143 | // Avoid super.visitSynchronizedStatement because it transforms the expression but then recurses on the entire 144 | // SynchronizedStatement, causing the expression to be visited a second time. 145 | sync.setExpression(transform(sync.getExpression())); 146 | try (StackVariableSet scope = new StackVariableSet(this)) { 147 | sync.getCode().visit(this); 148 | } 149 | } 150 | 151 | @Override 152 | public void visitTryCatchFinally(TryCatchStatement statement) { 153 | try (StackVariableSet scope = new StackVariableSet(this)) { 154 | super.visitTryCatchFinally(statement); 155 | } 156 | } 157 | 158 | @Override 159 | public void visitCatchStatement(CatchStatement statement) { 160 | try (StackVariableSet scope = new StackVariableSet(this)) { 161 | declareVariable(statement.getVariable()); 162 | super.visitCatchStatement(statement); 163 | } 164 | } 165 | 166 | @Override 167 | public void visitWhileLoop(WhileStatement loop) { 168 | // Avoid super.visitWhileLoop because it transforms the boolean expression but then recurses on the entire 169 | // WhileStatement, causing the boolean expression to be visited a second time. 170 | loop.setBooleanExpression((BooleanExpression) transform(loop.getBooleanExpression())); 171 | try (StackVariableSet scope = new StackVariableSet(this)) { 172 | loop.getLoopBlock().visit(this); 173 | } 174 | } 175 | 176 | /** 177 | * @see org.codehaus.groovy.classgen.asm.BinaryExpressionHelper#evaluateEqual(org.codehaus.groovy.ast.expr.BinaryExpression, boolean) 178 | */ 179 | void handleDeclarations(DeclarationExpression exp) { 180 | Expression leftExpression = exp.getLeftExpression(); 181 | if (leftExpression instanceof VariableExpression) { 182 | declareVariable((VariableExpression) leftExpression); 183 | } else if (leftExpression instanceof TupleExpression) { 184 | TupleExpression te = (TupleExpression) leftExpression; 185 | for (Expression e : te.getExpressions()) { 186 | declareVariable((VariableExpression)e); 187 | } 188 | } 189 | } 190 | 191 | void declareVariable(Variable exp) { 192 | varScope.declare(exp.getName()); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/FinalizerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2018 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.kohsuke.groovy.sandbox; 26 | 27 | import groovy.lang.GroovyShell; 28 | import org.codehaus.groovy.control.CompilerConfiguration; 29 | import org.codehaus.groovy.control.MultipleCompilationErrorsException; 30 | import org.codehaus.groovy.control.customizers.ImportCustomizer; 31 | import org.junit.Before; 32 | import org.junit.Test; 33 | import org.jvnet.hudson.test.Issue; 34 | 35 | import static org.hamcrest.CoreMatchers.anyOf; 36 | import static org.hamcrest.CoreMatchers.containsString; 37 | import static org.hamcrest.CoreMatchers.equalTo; 38 | import static org.hamcrest.CoreMatchers.instanceOf; 39 | import static org.junit.Assert.assertThat; 40 | import static org.junit.Assert.fail; 41 | 42 | @Issue("SECURITY-1186") 43 | public class FinalizerTest { 44 | private static final String SCRIPT_HARNESS = 45 | "class Global {\n" + 46 | " static volatile boolean result = false\n" + 47 | "}\n" + 48 | "class Test {\n" + 49 | " METHOD { Global.result = true; }\n" + 50 | "}\n" + 51 | "def t = new Test()\n" + 52 | "t = null\n" + 53 | // TODO: Flaky, can it be made more reliable? 54 | "for (int i = 0; i < 10 && Global.result == false; i++) {\n" + 55 | " System.gc()\n" + 56 | " System.runFinalization()\n" + 57 | " Thread.sleep(100)\n" + 58 | "}\n" + 59 | "Global.result"; 60 | 61 | private GroovyShell sandboxedSh; 62 | private GroovyShell unsandboxedSh; 63 | 64 | @Before 65 | public void setUp() { 66 | CompilerConfiguration cc = new CompilerConfiguration(); 67 | cc.addCompilationCustomizers(new ImportCustomizer().addImports("groovy.transform.PackageScope")); 68 | cc.addCompilationCustomizers(new SandboxTransformer()); 69 | sandboxedSh = new GroovyShell(cc); 70 | cc = new CompilerConfiguration(); 71 | cc.addCompilationCustomizers(new ImportCustomizer().addImports("groovy.transform.PackageScope")); 72 | unsandboxedSh = new GroovyShell(cc); 73 | } 74 | 75 | /** 76 | * These scripts are forbidden by {@link SandboxTransformer#call} after the SECURITY-1186 fix. 77 | */ 78 | @Test 79 | public void testOverridingFinalizeForbidden() { 80 | assertForbidden("@Override public void finalize()", true); 81 | assertForbidden("protected void finalize()", true); 82 | // Groovy's default access modifier is public. 83 | assertForbidden("void finalize()", true); 84 | assertForbidden("def void finalize()", true); 85 | // This finalizer would be invoked despite having @PackageScope, so it must be forbidden. 86 | assertForbidden("@PackageScope void finalize()", true); 87 | // Finalizers with only default parameters will cause a finalizer with no parameters to be 88 | // introduced, so they must be forbidden. 89 | assertForbidden("public void finalize(Object p1 = null)", true); 90 | assertForbidden("public void finalize(Object p1 = null, Object p2 = null)", true); 91 | assertForbidden("public void finalize(Object[] args = [null, null])", true); 92 | assertForbidden("public void finalize(Object... args = [null, null])", true); 93 | } 94 | 95 | /** 96 | * These scripts throw compilation failures even before the fix for SECURITY-1186 because they 97 | * are improper overrides of {@link Object#finalize}. 98 | */ 99 | @Test 100 | public void testImproperOverrideOfFinalize() { 101 | assertImproperOverride("private void finalize()"); 102 | assertImproperOverride("private static void finalize()"); 103 | assertImproperOverride("private Object finalize()"); 104 | assertImproperOverride("public Object finalize()"); 105 | assertImproperOverride("public Void finalize()"); 106 | assertImproperOverride("def finalize()"); 107 | } 108 | 109 | /** 110 | * These classes are allowed by {@link SandboxTransformer#call} because their finalize method 111 | * won't be invoked outside of the sandbox by the JVM. 112 | */ 113 | @Test 114 | public void testFinalizePermittedAsNonOverride() { 115 | assertFinalizerNotCalled("public static void finalize()"); 116 | assertFinalizerNotCalled("static void finalize()"); 117 | assertFinalizerNotCalled("protected static void finalize()"); 118 | assertFinalizerNotCalled("public void finalize(Object p)"); 119 | assertFinalizerNotCalled("protected void finalize(Object p)"); 120 | assertFinalizerNotCalled("private void finalize(Object p)"); 121 | assertFinalizerNotCalled("public void finalize(Object p1, Object p2 = null)"); 122 | assertFinalizerNotCalled("public void finalize(Object p1 = null, Object p2)"); 123 | assertFinalizerNotCalled("def void finalize(Map args)"); 124 | } 125 | 126 | private void assertForbidden(String methodStub, boolean isDangerous) { 127 | String script = SCRIPT_HARNESS.replace("METHOD", methodStub); 128 | ClassRecorder cr = new ClassRecorder(); 129 | cr.register(); 130 | try { 131 | sandboxedSh.evaluate(script); 132 | fail("Should have failed"); 133 | } catch (MultipleCompilationErrorsException e) { 134 | assertThat(e.getErrorCollector().getErrorCount(), equalTo(1)); 135 | Exception innerE = e.getErrorCollector().getException(0); 136 | assertThat(innerE, instanceOf(SecurityException.class)); 137 | assertThat(innerE.getMessage(), containsString("Object.finalize()")); 138 | } finally { 139 | cr.unregister(); 140 | } 141 | Object actual = unsandboxedSh.evaluate(script); 142 | assertThat(actual, equalTo((Object)isDangerous)); 143 | } 144 | 145 | private void assertImproperOverride(String methodStub) { 146 | ClassRecorder cr = new ClassRecorder(); 147 | cr.register(); 148 | try { 149 | sandboxedSh.evaluate(SCRIPT_HARNESS.replace("METHOD", methodStub)); 150 | fail("Should have failed"); 151 | } catch (MultipleCompilationErrorsException e) { 152 | assertThat(e.getErrorCollector().getErrorCount(), equalTo(1)); 153 | assertThat(e.getMessage(), anyOf( 154 | containsString("cannot override finalize in java.lang.Object"), 155 | containsString("incompatible with void in java.lang.Object"))); 156 | } finally { 157 | cr.unregister(); 158 | } 159 | } 160 | 161 | private void assertFinalizerNotCalled(String methodStub) { 162 | ClassRecorder cr = new ClassRecorder(); 163 | cr.register(); 164 | try { 165 | Boolean actual = (Boolean)sandboxedSh.evaluate(SCRIPT_HARNESS.replace("METHOD", methodStub)); 166 | assertThat(actual, equalTo(false)); 167 | } finally { 168 | cr.unregister(); 169 | } 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/GroovyInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import org.kohsuke.groovy.sandbox.impl.Super; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.concurrent.CopyOnWriteArrayList; 8 | 9 | /** 10 | * Interceptor of Groovy method calls. 11 | * 12 | *

13 | * Once created, it needs to be {@linkplain #register() registered} to start receiving interceptions. 14 | * List of interceptors are maintained per thread. 15 | * 16 | * @author Kohsuke Kawaguchi 17 | */ 18 | public abstract class GroovyInterceptor { 19 | /** 20 | * Intercepts an instance method call on some object of the form "foo.bar(...)" 21 | */ 22 | public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable { 23 | return invoker.call(receiver,method,args); 24 | } 25 | 26 | /** 27 | * Intercepts a static method call on some class, like "Class.forName(...)". 28 | * 29 | * Note that Groovy doesn't clearly differentiate static method calls from instance method calls. 30 | * If calls are determined to be static at compile-time, you get this method called, but 31 | * method calls whose receivers are {@link Class} can invoke static methods, too 32 | * (that is, {@code x=Integer.class;x.valueOf(5)} results in {@code onMethodCall(invoker,Integer.class,"valueOf",5)} 33 | */ 34 | public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable { 35 | return invoker.call(receiver,method,args); 36 | } 37 | 38 | /** 39 | * Intercepts an object instantiation, like "new Receiver(...)" 40 | */ 41 | public Object onNewInstance(Invoker invoker, Class receiver, Object... args) throws Throwable { 42 | return invoker.call(receiver,null,args); 43 | } 44 | 45 | /** 46 | * Intercepts an super method call, like "super.foo(...)" 47 | */ 48 | public Object onSuperCall(Invoker invoker, Class senderType, Object receiver, String method, Object... args) throws Throwable { 49 | return invoker.call(new Super(senderType,receiver),method,args); 50 | } 51 | 52 | /** 53 | * Intercepts a {@code super(…)} call from a constructor. 54 | */ 55 | public void onSuperConstructor(Invoker invoker, Class receiver, Object... args) throws Throwable { 56 | onNewInstance(invoker, receiver, args); 57 | } 58 | 59 | /** 60 | * Intercepts a property access, like "z=foo.bar" 61 | * 62 | * @param receiver 63 | * 'foo' in the above example, the object whose property is accessed. 64 | * @param property 65 | * 'bar' in the above example, the name of the property 66 | */ 67 | public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable { 68 | return invoker.call(receiver,property); 69 | } 70 | 71 | /** 72 | * Intercepts a property assignment like "foo.bar=z" 73 | * 74 | * @param receiver 75 | * 'foo' in the above example, the object whose property is accessed. 76 | * @param property 77 | * 'bar' in the above example, the name of the property 78 | * @param value 79 | * The value to be assigned. 80 | * @return 81 | * The result of the assignment expression. Normally, you should return the same object as {@code value}. 82 | */ 83 | public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable { 84 | return invoker.call(receiver,property,value); 85 | } 86 | 87 | /** 88 | * Intercepts an attribute access, like "z=foo.@bar" 89 | * 90 | * @param receiver 91 | * 'foo' in the above example, the object whose attribute is accessed. 92 | * @param attribute 93 | * 'bar' in the above example, the name of the attribute 94 | */ 95 | public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable { 96 | return invoker.call(receiver, attribute); 97 | } 98 | 99 | /** 100 | * Intercepts an attribute assignment like "foo.@bar=z" 101 | * 102 | * @param receiver 103 | * 'foo' in the above example, the object whose attribute is accessed. 104 | * @param attribute 105 | * 'bar' in the above example, the name of the attribute 106 | * @param value 107 | * The value to be assigned. 108 | * @return 109 | * The result of the assignment expression. Normally, you should return the same object as {@code value}. 110 | */ 111 | public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable { 112 | return invoker.call(receiver,attribute,value); 113 | } 114 | 115 | /** 116 | * Intercepts an array access, like "z=foo[bar]" 117 | * 118 | * @param receiver 119 | * 'foo' in the above example, the array-like object. 120 | * @param index 121 | * 'bar' in the above example, the object that acts as an index. 122 | */ 123 | public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable { 124 | return invoker.call(receiver,null,index); 125 | } 126 | 127 | /** 128 | * Intercepts an attribute assignment like "foo[bar]=z" 129 | * 130 | * @param receiver 131 | * 'foo' in the above example, the array-like object. 132 | * @param index 133 | * 'bar' in the above example, the object that acts as an index. 134 | * @param value 135 | * The value to be assigned. 136 | * @return 137 | * The result of the assignment expression. Normally, you should return the same object as {@code value}. 138 | */ 139 | public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable { 140 | return invoker.call(receiver,null,index,value); 141 | } 142 | 143 | /** 144 | * Represents the next interceptor in the chain. 145 | * 146 | * As {@link GroovyInterceptor}, you intercept by doing one of the following: 147 | * 148 | *

    149 | *
  • Pass on to the next interceptor by calling one of the call() method, 150 | * possibly modifying the arguments and return values, intercepting an exception, etc. 151 | *
  • Throws an exception to block the call. 152 | *
  • Return some value without calling the next interceptor. 153 | *
154 | * 155 | * The signature of the call method is as follows: 156 | * 157 | *
158 | *
receiver
159 | *
160 | * The object whose method/property is accessed. 161 | * For constructor invocations and static calls, this is {@link Class}. 162 | * If the receiver is null, all the interceptors will be skipped. 163 | *
164 | *
method
165 | *
166 | * The name of the method/property/attribute. Otherwise pass in null. 167 | *
168 | *
args
169 | *
170 | * Arguments of the method call, index of the array access, and/or values to be set. 171 | * Multiple override of the call method is provided to avoid the implicit object 172 | * array creation, but otherwise they behave the same way. 173 | *
174 | *
175 | */ 176 | public interface Invoker { 177 | Object call(Object receiver, String method) throws Throwable; 178 | Object call(Object receiver, String method, Object arg1) throws Throwable; 179 | Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable; 180 | Object call(Object receiver, String method, Object... args) throws Throwable; 181 | } 182 | 183 | // public void addToGlobal() { 184 | // globalInterceptors.add(this); 185 | // } 186 | // 187 | // public void removeFromGlobal() { 188 | // globalInterceptors.remove(this); 189 | // } 190 | 191 | /** 192 | * Registers this interceptor to the current thread's interceptor list. 193 | */ 194 | public void register() { 195 | threadInterceptors.get().add(this); 196 | } 197 | 198 | /** 199 | * Reverses the earlier effect of {@link #register()} 200 | */ 201 | public void unregister() { 202 | threadInterceptors.get().remove(this); 203 | } 204 | 205 | private static final ThreadLocal> threadInterceptors = new ThreadLocal>() { 206 | @Override 207 | protected List initialValue() { 208 | return new CopyOnWriteArrayList(); 209 | } 210 | }; 211 | 212 | private static final ThreadLocal> threadInterceptorsView = new ThreadLocal>() { 213 | @Override 214 | protected List initialValue() { 215 | return Collections.unmodifiableList(threadInterceptors.get()); 216 | } 217 | }; 218 | 219 | // private static final List globalInterceptors = new CopyOnWriteArrayList(); 220 | 221 | public static List getApplicableInterceptors() { 222 | return threadInterceptorsView.get(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/test/java/org/kohsuke/groovy/sandbox/TheTest.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import org.codehaus.groovy.runtime.NullObject; 4 | import org.codehaus.groovy.runtime.ProxyGeneratorAdapter; 5 | import org.jvnet.hudson.test.Issue; 6 | import java.awt.Point; 7 | import java.io.File; 8 | import java.io.PrintWriter; 9 | import java.io.StringWriter; 10 | import java.lang.reflect.Field; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.Locale; 14 | import java.util.concurrent.atomic.AtomicLong; 15 | import org.codehaus.groovy.runtime.ResourceGroovyMethods; 16 | import org.junit.Test; 17 | 18 | import static org.hamcrest.CoreMatchers.containsString; 19 | import static org.hamcrest.CoreMatchers.instanceOf; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.junit.Assert.assertEquals; 22 | 23 | /** 24 | * 25 | * 26 | * @author Kohsuke Kawaguchi 27 | */ 28 | public class TheTest extends SandboxTransformerTest { 29 | @Override 30 | public void configureBinding() { 31 | binding.setProperty("foo", "FOO"); 32 | binding.setProperty("bar", "BAR"); 33 | binding.setProperty("zot", 5); 34 | binding.setProperty("point", new Point(1, 2)); 35 | binding.setProperty("points", Arrays.asList(new Point(1, 2), new Point(3, 4))); 36 | binding.setProperty("intArray", new int[] { 0, 1, 2, 3, 4 }); 37 | } 38 | 39 | private void assertIntercept(String expectedCallSequence, Object expectedValue, String script) throws Exception { 40 | String[] expectedCalls = expectedCallSequence.isEmpty() ? new String[0] : expectedCallSequence.split("/"); 41 | assertIntercept(script, expectedValue, expectedCalls); 42 | } 43 | 44 | private void assertInterceptNoScript(String expectedCallSequence, Object expectedValue, String script) throws Exception { 45 | String[] expectedCalls = expectedCallSequence.isEmpty() ? new String[0] : expectedCallSequence.split("/"); 46 | assertEvaluate(script, expectedValue); 47 | assertInterceptedExact(expectedCalls); 48 | } 49 | 50 | private void assertIntercept(List expectedCallSequence, Object expectedValue, String script) throws Exception { 51 | assertIntercept(script, expectedValue, expectedCallSequence.toArray(new String[0])); 52 | } 53 | 54 | @Test public void testOK() throws Exception { 55 | // instance call 56 | assertIntercept( 57 | "Integer.class/Class:forName(String)", 58 | String.class, 59 | "5.class.forName('java.lang.String')"); 60 | 61 | assertIntercept( 62 | "String.toString()/String.hashCode()", 63 | "foo".hashCode(), 64 | "'foo'.toString().hashCode()" 65 | ); 66 | 67 | // static call 68 | assertIntercept(// turns out this doesn't actually result in onStaticCall 69 | "Math:max(Float,Float)", 70 | Math.max(1f,2f), 71 | "Math.max(1f,2f)" 72 | ); 73 | 74 | assertIntercept(// ... but this does 75 | "Math:max(Float,Float)", 76 | Math.max(1f,2f), 77 | "import static java.lang.Math.*; max(1f,2f)" 78 | ); 79 | 80 | // property access 81 | assertIntercept( 82 | "String.class/Class.name", 83 | String.class.getName(), 84 | "'foo'.class.name" 85 | ); 86 | 87 | // constructor & field access 88 | assertIntercept( 89 | "new Point(Integer,Integer)/Point.@x", 90 | 1, 91 | "new java.awt.Point(1,2).@x" 92 | ); 93 | 94 | // property set 95 | assertIntercept( 96 | "Script7.point/Point.x=Integer", 97 | 3, 98 | "point.x=3" 99 | ); 100 | assertEquals(3, ((Point)binding.getProperty("point")).x); 101 | 102 | // attribute set 103 | assertIntercept( 104 | "Script8.point/Point.@x=Integer", 105 | 4, 106 | "point.@x=4" 107 | ); 108 | assertEquals(4, ((Point)binding.getProperty("point")).x); 109 | 110 | // property spread 111 | assertIntercept( 112 | "Script9.points/Point.x=Integer/Point.x=Integer", 113 | 3, 114 | "points*.x=3" 115 | ); 116 | assertEquals(3, ((List)binding.getProperty("points")).get(0).x); 117 | assertEquals(3, ((List)binding.getProperty("points")).get(1).x); 118 | 119 | // array set & get 120 | assertIntercept( 121 | "int[][Integer]=Integer/int[][Integer]", 122 | 1, 123 | "def x=new int[3];x[0]=1;x[0]" 124 | ); 125 | } 126 | 127 | @Test public void testClosure() throws Exception { 128 | assertIntercept( 129 | "Script1$_run_closure1.call()/Integer.class/Class:forName(String)", 130 | null, 131 | "def foo = { 5.class.forName('java.lang.String') }\n" + 132 | "foo()\n" + 133 | "return null"); 134 | } 135 | 136 | @Test public void testClass() throws Exception { 137 | assertInterceptNoScript( 138 | "Integer.class/Class:forName(String)", 139 | null, 140 | "class foo { static void main(String[] args) throws Exception { 5.class.forName('java.lang.String') } }"); 141 | } 142 | 143 | @Test public void testInnerClass() throws Exception { 144 | assertInterceptNoScript( 145 | "foo$bar:juu()/Integer.class/Class:forName(String)", 146 | null, 147 | "class foo {\n" + 148 | " class bar {\n" + 149 | " static void juu() throws Exception { 5.class.forName('java.lang.String') }\n" + 150 | " }\n" + 151 | "static void main(String[] args) throws Exception { bar.juu() }\n" + 152 | "}"); 153 | } 154 | 155 | @Test public void testStaticInitializationBlock() throws Exception { 156 | assertInterceptNoScript( 157 | "Integer.class/Class:forName(String)", 158 | null, 159 | "class foo {\n" + 160 | "static { 5.class.forName('java.lang.String') }\n" + 161 | " static void main(String[] args) throws Exception { }\n" + 162 | "}"); 163 | } 164 | 165 | @Test public void testConstructor() throws Exception { 166 | assertIntercept( 167 | "new foo()/Integer.class/Class:forName(String)", 168 | null, 169 | "class foo {\n" + 170 | "foo() { 5.class.forName('java.lang.String') }\n" + 171 | "}\n" + 172 | "new foo()\n" + 173 | "return null"); 174 | } 175 | 176 | @Test public void testInitializationBlock() throws Exception { 177 | assertIntercept( 178 | "new foo()/Integer.class/Class:forName(String)", 179 | null, 180 | "class foo {\n" + 181 | "{ 5.class.forName('java.lang.String') }\n" + 182 | "}\n" + 183 | "new foo()\n" + 184 | "return null"); 185 | } 186 | 187 | @Test public void testFieldInitialization() throws Exception { 188 | assertIntercept( 189 | "new foo()/Integer.class/Class:forName(String)", 190 | null, 191 | "class foo {\n" + 192 | "def obj = 5.class.forName('java.lang.String')\n" + 193 | "}\n" + 194 | "new foo()\n" + 195 | "return null"); 196 | } 197 | 198 | @Test public void testStaticFieldInitialization() throws Exception { 199 | assertIntercept( 200 | "new foo()/Integer.class/Class:forName(String)", 201 | null, 202 | "class foo {\n" + 203 | "static obj = 5.class.forName('java.lang.String')\n" + 204 | "}\n" + 205 | "new foo()\n" + 206 | "return null"); 207 | } 208 | 209 | @Test public void testCompoundAssignment() throws Exception { 210 | assertIntercept( 211 | "Script1.point/Point.x/Double.plus(Integer)/Point.x=Double", 212 | (double)4.0, 213 | "point.x += 3"); 214 | } 215 | 216 | @Test public void testCompoundAssignment2() throws Exception { 217 | // "[I" is the type name of int[] 218 | assertIntercept( 219 | "Script1.intArray/int[][Integer]/Integer.leftShift(Integer)/int[][Integer]=Integer", 220 | 1<<3, 221 | "intArray[1] <<= 3"); 222 | } 223 | 224 | @Test public void testComparison() throws Exception { 225 | assertIntercept( 226 | "Script1.point/Script1.point/Point.equals(Point)/Integer.compareTo(Integer)", 227 | true, 228 | "point==point; 5==5"); 229 | } 230 | 231 | @Test public void testAnonymousClass() throws Exception { 232 | assertIntercept( 233 | "new Script1$1(Script1)/Script1$1.@this$0=Script1/Script1$1.plusOne(Integer)/Integer.plus(Integer)", 234 | 6, 235 | "def x = new Object() {\n" + 236 | " def plusOne(rhs) {\n" + 237 | " return rhs+1\n" + 238 | " }\n" + 239 | "}\n" + 240 | "x.plusOne(5)\n"); 241 | } 242 | 243 | @Test public void testIssue2() throws Exception { 244 | assertIntercept("new HashMap()/HashMap.get(String)/Script1.nop(null)",null,"def nop(v) { }; nop(new HashMap().dummy);"); 245 | assertIntercept("Script2.nop()",null,"def nop() { }; nop();"); 246 | assertIntercept("Script3.nop(null)",null,"def nop(v) { }; nop(null);"); 247 | } 248 | 249 | @Test public void testSystemExitAsFunction() throws Exception { 250 | assertIntercept("TheTest:idem(Integer)/TheTest:idem(Integer)",123,"org.kohsuke.groovy.sandbox.TheTest.idem(org.kohsuke.groovy.sandbox.TheTest.idem(123))"); 251 | } 252 | 253 | /** 254 | * Idempotent function used for testing 255 | */ 256 | public static Object idem(Object o) { 257 | return o; 258 | } 259 | 260 | @Test public void testArrayArgumentsInvocation() throws Exception { 261 | assertIntercept( 262 | "new TheTest$MethodWithArrayArg()/TheTest$MethodWithArrayArg.f(Object[])", 263 | 3, 264 | "new TheTest.MethodWithArrayArg().f(new Object[3])"); 265 | } 266 | 267 | public static class MethodWithArrayArg { 268 | public Object f(Object[] arg) { 269 | return arg.length; 270 | } 271 | } 272 | 273 | /** 274 | * See issue #6. We are not intercepting calls to null. 275 | */ 276 | @Test public void testNull() throws Exception { 277 | assertIntercept("", NullObject.class, "def x=null; null.getClass()"); 278 | assertIntercept("", "null3", "def x=null; x.plus('3')"); 279 | assertIntercept("", false, "def x=null; x==3"); 280 | } 281 | 282 | /** 283 | * See issue #9 284 | */ 285 | @Test public void testAnd() throws Exception { 286 | assertIntercept("", false, 287 | "String s = null\n" + 288 | "if (s != null && s.length > 0)\n" + 289 | " throw new Exception()\n" + 290 | "return false\n"); 291 | } 292 | 293 | @Test public void testLogicalNotEquals() throws Exception { 294 | assertIntercept("Integer.toString()/String.compareTo(String)", true, 295 | "def x = 3.toString(); if (x != '') return true; else return false;"); 296 | } 297 | 298 | // see issue 8 299 | @Test public void testClosureDelegation() throws Exception { 300 | assertIntercept(Arrays.asList 301 | ( 302 | "Script1$_run_closure1.call()", 303 | "Script1$_run_closure1.delegate=String", 304 | "String.length()" 305 | ), 3, 306 | "def x = 0\n" + 307 | "def c = { ->\n" + 308 | " delegate = 'foo'\n" + 309 | " x = length()\n" + 310 | "}\n" + 311 | "c()\n" + 312 | "x\n"); 313 | } 314 | 315 | @Test public void testClosureDelegationOwner() throws Exception { 316 | assertIntercept(Arrays.asList 317 | ( 318 | "Script1$_run_closure1.call()", 319 | "Script1$_run_closure1.delegate=String", 320 | "Script1$_run_closure1$_closure2.call()", 321 | "String.length()" 322 | ), 323 | 3, 324 | "def x = 0\n" + 325 | "def c = { ->\n" + 326 | " delegate = 'foo';\n" + 327 | " { -> x = length() }()\n" + 328 | "}\n" + 329 | "c()\n" + 330 | "x\n"); 331 | } 332 | 333 | @Test public void testClosureDelegationProperty() throws Exception { 334 | // TODO: ideally we should be seeing String.length() 335 | // doing so requires a call site selection and deconstruction 336 | assertIntercept(Arrays.asList 337 | ( 338 | "Script1$_run_closure1.call()", 339 | "new SomeBean(Integer,Integer)", 340 | "Script1$_run_closure1.delegate=SomeBean", 341 | // by the default delegation rule of Closure, it first attempts to get Script1.x, 342 | // and only after we find out that there's no such property, we fall back to SomeBean.x 343 | "Script1.x", 344 | "SomeBean.x", 345 | "Script1.y", 346 | "SomeBean.y", 347 | "Integer.plus(Integer)" 348 | ), 349 | 3, 350 | "def sum = 0\n" + 351 | "def c = { ->\n" + 352 | " delegate = new SomeBean(1,2)\n" + 353 | " sum = x+y\n" + 354 | "}\n" + 355 | "c()\n" + 356 | "sum\n"); 357 | } 358 | 359 | @Test public void testClosureDelegationPropertyDelegateOnly() throws Exception { 360 | assertIntercept(Arrays.asList 361 | ( 362 | "Script1$_run_closure1.call()", 363 | "new SomeBean(Integer,Integer)", 364 | "Script1$_run_closure1.delegate=SomeBean", 365 | "Script1$_run_closure1.resolveStrategy=Integer", 366 | // with DELEGATE_FIRST rule, unlike testClosureDelegationProperty() it shall not touch Script1.* 367 | "SomeBean.x", 368 | "SomeBean.y", 369 | "Integer.plus(Integer)" 370 | ), 371 | 3, 372 | "def sum = 0\n" + 373 | "def c = { ->\n" + 374 | " delegate = new SomeBean(1,2)\n" + 375 | " resolveStrategy = 1; // Closure.DELEGATE_FIRST\n" + 376 | " sum = x+y\n" + 377 | "}\n" + 378 | "c()\n" + 379 | "sum\n"); 380 | } 381 | 382 | @Test public void testClosureDelegationPropertyOwner() throws Exception { 383 | /* 384 | The way property access of 'x' gets dispatched to is: 385 | 386 | innerClosure.getProperty("x"), which delegates to its owner, which is 387 | outerClosure.getProperty("x"), which delegates to its delegate, which is 388 | SomeBean.x 389 | */ 390 | assertIntercept(Arrays.asList 391 | ( 392 | "Script1$_run_closure1.call()", 393 | "new SomeBean(Integer,Integer)", 394 | "Script1$_run_closure1.delegate=SomeBean", 395 | "Script1$_run_closure1$_closure2.call()", 396 | "Script1.x", 397 | "SomeBean.x", 398 | "Script1.y", 399 | "SomeBean.y", 400 | "Integer.plus(Integer)" 401 | ), 402 | 3, 403 | "def sum = 0\n" + 404 | "def c = { ->\n" + 405 | " delegate = new SomeBean(1,2);\n" + 406 | " { -> sum = x+y; }()\n" + 407 | "}\n" + 408 | "c()\n" + 409 | "sum\n"); 410 | } 411 | 412 | @Test public void testGString() throws Exception { 413 | assertIntercept("Integer.plus(Integer)/Integer.plus(Integer)/GStringImpl.toString()", "answer=6", 414 | "def x = /answer=${1+2+3}/; x.toString()"); 415 | } 416 | 417 | @Test public void testClosurePropertyAccess() throws Exception { 418 | assertIntercept(Arrays.asList( 419 | "Script1$_run_closure1.call()", 420 | "new Exception(String)", 421 | "Script1$_run_closure1.delegate=Exception", 422 | "Script1.message", 423 | "Exception.message"), 424 | "foo", 425 | "{ ->\n" + 426 | " delegate = new Exception('foo')\n" + 427 | " return message\n" + 428 | "}()\n"); 429 | } 430 | 431 | /** 432 | * Calling method on Closure that's not delegated to somebody else. 433 | */ 434 | @Test public void testNonDelegatingClosure() throws Exception { 435 | assertIntercept(Arrays.asList( 436 | "Script1$_run_closure1.hashCode()", 437 | "Script1$_run_closure1.equals(Script1$_run_closure1)" 438 | ), true, 439 | "def c = { -> }\n" + 440 | "c.hashCode()\n" + 441 | "c.equals(c)\n"); 442 | 443 | // but these guys are not on closure 444 | assertIntercept(Arrays.asList( 445 | "Script2$_run_closure1.call()", 446 | "Script2$_run_closure1.hashCode()", 447 | "Script2$_run_closure1.hashCode()", 448 | "Integer.compareTo(Integer)" 449 | ), true, 450 | "def c = { ->\n" + 451 | " hashCode()\n" + 452 | "}\n" + 453 | "return c()==c.hashCode()\n"); 454 | } 455 | 456 | // Groovy doesn't allow this? 457 | // void testLocalClass() { 458 | // assertIntercept( 459 | // "new Foo()/Foo.plusOne(Integer)/Integer.plus(Integer)", 460 | // 7, 461 | //""" 462 | //class Foo { 463 | // def plusTwo(rhs) { 464 | // class Bar { def plusOne(rhs) { rhs + 2; } } 465 | // return new Bar().plusOne(rhs)+1; 466 | // } 467 | //} 468 | //new Foo().plusTwo(5) 469 | //""") 470 | // } 471 | 472 | // bug 14 473 | @Test public void testUnclassifiedStaticMethod() throws Exception { 474 | assertIntercept(Arrays.asList 475 | ( 476 | "Script1.m()", 477 | "System:getProperty(String)" 478 | ),null, 479 | "m()\n" + 480 | "def m() {\n" + 481 | " System.getProperty('foo')\n" + 482 | "}\n"); 483 | } 484 | 485 | @Test public void testInstanceOf() throws Exception { 486 | assertIntercept("", true, 487 | "def x = 'foo'\n" + 488 | "x instanceof String\n"); 489 | } 490 | 491 | @Test public void testRegexp() throws Exception { 492 | assertIntercept(Arrays.asList 493 | ( 494 | "ScriptBytecodeAdapter:findRegex(String,String)", 495 | "ScriptBytecodeAdapter:matchRegex(String,String)" 496 | ), false, 497 | "def x = 'foo'\n" + 498 | "x =~ /bla/\n" + 499 | "x ==~ /bla/\n"); 500 | } 501 | 502 | @Issue("JENKINS-46088") 503 | @Test public void testMatcherTypeAssignment() throws Exception { 504 | assertIntercept(Arrays.asList 505 | ( 506 | "ScriptBytecodeAdapter:findRegex(String,String)", 507 | "Matcher.matches()" 508 | ), false, 509 | "def x = 'foo'\n" + 510 | "java.util.regex.Matcher m = x =~ /bla/\n" + 511 | "return m.matches()\n"); 512 | } 513 | 514 | @Test public void testNumericComparison() throws Exception { 515 | assertIntercept("Integer.compareTo(Integer)", true, 516 | "5 < 8"); 517 | } 518 | 519 | @Test public void testIssue17() throws Exception { 520 | assertIntercept("new IntRange(Boolean,Integer,Integer)", 45, 521 | "def x = 0\n" + 522 | "for ( i in 0..9 ) {\n" + 523 | " x+= i\n" + 524 | "}\n" + 525 | "return x\n"); 526 | } 527 | 528 | // issue 16 529 | @Test public void testPrePostfixLocalVariable() throws Exception { 530 | assertIntercept("Integer.next()/ArrayList[Integer]", Arrays.asList(1, 0), 531 | "def x = 0\n" + 532 | "def y=x++\n" + 533 | "return [x,y]"); 534 | 535 | assertIntercept("Integer.previous()", Arrays.asList(2, 2), 536 | "def x = 3\n" + 537 | "def y=--x\n" + 538 | "return [x,y]"); 539 | } 540 | 541 | @Test public void testPrePostfixArray() throws Exception { 542 | assertIntercept(Arrays.asList( 543 | "ArrayList[Integer]", // for reading x[1] before increment 544 | "Integer.next()", 545 | "ArrayList[Integer]=Integer", // for writing x[1] after increment 546 | "ArrayList[Integer]" // for reading x[1] in the return statement 547 | ), Arrays.asList(3, 2), 548 | "def x = [1,2,3]\n" + 549 | "def y=x[1]++\n" + 550 | "return [x[1],y]"); 551 | 552 | assertIntercept(Arrays.asList( 553 | "ArrayList[Integer]", // for reading x[1] before increment 554 | "Integer.previous()", 555 | "ArrayList[Integer]=Integer", // for writing x[1] after increment 556 | "ArrayList[Integer]" // for reading x[1] in the return statement 557 | ), Arrays.asList(1, 1), 558 | "def x = [1,2,3]\n" + 559 | "def y=--x[1]\n" + 560 | "return [x[1],y]"); 561 | } 562 | 563 | @Test public void testPrePostfixProperty() throws Exception { 564 | assertIntercept(Arrays.asList( 565 | "Script1.x=Integer", // x=3 566 | "Script1.x", 567 | "Integer.next()", 568 | "Script1.x=Integer", // read, plus, then write back 569 | "Script1.x" // final read for the return statement 570 | ), Arrays.asList(4, 3), 571 | "x = 3\n" + 572 | "def y=x++\n" + 573 | "return [x,y]\n"); 574 | 575 | assertIntercept(Arrays.asList( 576 | "Script2.x=Integer", // x=3 577 | "Script2.x", 578 | "Integer.previous()", 579 | "Script2.x=Integer", // read, plus, then write back 580 | "Script2.x" // final read for the return statement 581 | ), Arrays.asList(2, 2), 582 | "x = 3\n" + 583 | "def y=--x\n" + 584 | "return [x,y]\n"); 585 | } 586 | 587 | @Test public void testCatchStatement() throws Exception { 588 | sandboxedEval( 589 | "def o = null\n" + 590 | "try {\n" + 591 | " o.hello()\n" + 592 | " return null\n" + 593 | "} catch (Exception e) {\n" + 594 | " throw new Exception('wrapped', e)\n" + 595 | "}", 596 | ShouldFail.class, 597 | e -> { 598 | assertThat(e.getMessage(), containsString("wrapped")); 599 | assertThat(e.getCause(), instanceOf(NullPointerException.class)); 600 | }); 601 | } 602 | 603 | /** 604 | * Makes sure the line number in the source code is preserved after translation. 605 | */ 606 | @Test public void testIssue21() throws Exception { 607 | sandboxedEval( 608 | "\n" + // line 1 609 | "def x = null\n" + 610 | "def cl = {\n" + 611 | " x.hello()\n" + // line 4 612 | "}\n" + 613 | "try {\n" + 614 | " cl();\n" + // line 7 615 | "} catch (Exception e) {\n" + 616 | " throw new Exception('wrapped', e)\n" + 617 | "}", 618 | ShouldFail.class, 619 | e -> { 620 | assertThat(e.getMessage(), containsString("wrapped")); 621 | StringWriter sw = new StringWriter(); 622 | e.printStackTrace(new PrintWriter(sw)); 623 | 624 | String s = sw.toString(); 625 | assertThat(s, containsString("Script1.groovy:4")); 626 | assertThat(s, containsString("Script1.groovy:7")); 627 | }); 628 | } 629 | 630 | @Test public void testIssue15() throws Exception { 631 | sandboxedEval( 632 | "try {\n" + 633 | " def x = null\n" + 634 | " return x.nullProp\n" + 635 | "} catch (Exception e) {\n" + 636 | " throw new Exception('wrapped', e)\n" + 637 | "}", 638 | ShouldFail.class, 639 | e -> { 640 | assertThat(e.getMessage(), containsString("wrapped")); 641 | assertThat(e.getCause(), instanceOf(NullPointerException.class)); 642 | }); 643 | // x.nullProp shouldn't be intercepted 644 | assertIntercepted("new Exception(String,NullPointerException)"); 645 | 646 | sandboxedEval( 647 | "try {\n" + 648 | " def x = null\n" + 649 | " x.nullProp = 1\n" + 650 | "} catch (Exception e) {\n" + 651 | " throw new Exception('wrapped', e)\n" + 652 | "}", 653 | ShouldFail.class, 654 | e -> { 655 | assertThat(e.getMessage(), containsString("wrapped")); 656 | assertThat(e.getCause(), instanceOf(NullPointerException.class)); 657 | }); 658 | // x.nullProp shouldn't be intercepted 659 | assertIntercepted("new Exception(String,NullPointerException)"); 660 | } 661 | 662 | @Test public void testInOperator() throws Exception { 663 | assertIntercept( 664 | "Integer.isCase(Integer)", true, "1 in 1" 665 | ); 666 | 667 | assertIntercept( 668 | "Integer.isCase(Integer)", false, "1 in 2" 669 | ); 670 | 671 | assertIntercept( 672 | "ArrayList.isCase(Integer)", true, "1 in [1]" 673 | ); 674 | 675 | assertIntercept( 676 | "ArrayList.isCase(Integer)", false, "1 in [2]" 677 | ); 678 | } 679 | 680 | /** 681 | * Property access to Map is handled specially by MetaClassImpl, so our interceptor needs to treat that 682 | * accordingly. 683 | */ 684 | @Test public void testMapPropertyAccess() throws Exception { 685 | assertIntercept("new HashMap()/HashMap.get(String)",null,"new HashMap().dummy;"); 686 | assertIntercept("new HashMap()/HashMap.put(String,Integer)",5,"new HashMap().dummy=5"); 687 | } 688 | 689 | /** 690 | * Intercepts super.toString() 691 | */ 692 | @Issue("JENKINS-42563") 693 | @Test public void testSuperCall() throws Exception { 694 | assertIntercept(Arrays.asList( 695 | "new Zot()", 696 | "new Bar()", 697 | "new Foo()", 698 | "Zot.toString()", 699 | "Zot.super(Bar).toString()", 700 | "String.plus(String)" 701 | ), "xfoo", 702 | "class Foo {\n" + 703 | " public String toString() {\n" + 704 | " return 'foo'\n" + 705 | " }\n" + 706 | "}\n" + 707 | "class Bar extends Foo {\n" + 708 | " public String toString() {\n" + 709 | " return 'x'+super.toString()\n" + 710 | " }\n" + 711 | "}\n" + 712 | "class Zot extends Bar {}\n" + 713 | "new Zot().toString()\n"); 714 | } 715 | 716 | @Test public void testPostfixOpInClosure() throws Exception { 717 | assertIntercept(Arrays.asList( 718 | "ArrayList.each(Script1$_run_closure1)", 719 | "Integer.next()", 720 | "ArrayList[Integer]", 721 | "Integer.next()", 722 | "ArrayList[Integer]", 723 | "Integer.next()", 724 | "ArrayList[Integer]", 725 | "Integer.next()", 726 | "ArrayList[Integer]", 727 | "Integer.next()", 728 | "ArrayList[Integer]"), 729 | 5, 730 | "def cnt = 0\n" + 731 | "[0, 1, 2, 3, 4].each {\n" + 732 | " cnt++\n" + 733 | "}\n" + 734 | "return cnt\n"); 735 | } 736 | 737 | @Issue("SECURITY-566") 738 | @Test public void testTypeCoercion() throws Exception { 739 | Field pxyCounterField = ProxyGeneratorAdapter.class.getDeclaredField("pxyCounter"); 740 | pxyCounterField.setAccessible(true); 741 | AtomicLong pxyCounterValue = (AtomicLong) pxyCounterField.get(null); 742 | pxyCounterValue.set(0); // make sure *_groovyProxy names are predictable 743 | assertIntercept("Locale:getDefault()/Class1_groovyProxy.getDefault()", 744 | Locale.getDefault(), 745 | "interface I {\n" + 746 | " Locale getDefault()\n" + 747 | "}\n" + 748 | "(Locale as I).getDefault()\n"); 749 | } 750 | 751 | @Issue("JENKINS-33468") 752 | @Test public void testClosureImplicitIt() throws Exception { 753 | assertIntercept(Arrays.asList( 754 | "Script1.c=Script1$_run_closure1", 755 | "Script1.c(Integer)", 756 | "Integer.plus(Integer)" 757 | ), 2, 758 | "c = { it + 1 }\n" + 759 | "c(1)\n" 760 | ); 761 | 762 | assertIntercept(Arrays.asList( 763 | "Script2.c=Script2$_run_closure1", 764 | "Script2.c(Integer)", 765 | "Integer.plus(Integer)" 766 | ), 2, 767 | "c = {v -> v + 1 }\n" + 768 | "c(1)\n" 769 | ); 770 | 771 | assertIntercept(Arrays.asList( 772 | "Script3.c=Script3$_run_closure1", 773 | "Script3.c()" 774 | ), 2, 775 | "c = {-> 2 }\n" + 776 | "c()" 777 | ); 778 | } 779 | 780 | @Issue("JENKINS-46191") 781 | @Test public void testEmptyDeclaration() throws Exception { 782 | assertIntercept("", 783 | "abc", 784 | "String a\n" + 785 | "a = 'abc'\n" + 786 | "return a\n"); 787 | } 788 | 789 | @Issue("SECURITY-663") 790 | @Test public void testAsFile() throws Exception { 791 | File f = File.createTempFile("foo", ".tmp"); 792 | 793 | ResourceGroovyMethods.write(f, "This is\na test\n"); 794 | assertIntercept(Arrays.asList( 795 | "new File(String)", 796 | "File.each(Script1$_run_closure1)", 797 | "ArrayList.leftShift(String)", 798 | "ArrayList.leftShift(String)", 799 | "ArrayList.join(String)"), 800 | "This is a test", 801 | "def s = []\n" + 802 | "($/" + f.getCanonicalPath() + "/$ as File).each { s << it }\n" + 803 | "s.join(' ')\n"); 804 | } 805 | 806 | @Issue("JENKINS-50380") 807 | @Test public void testCheckedCastWhenAssignable() throws Exception { 808 | assertIntercept("new NonArrayConstructorList(Boolean,Boolean)/NonArrayConstructorList.join(String)", 809 | "one", 810 | "NonArrayConstructorList foo = new NonArrayConstructorList(true, false)\n" + 811 | "List castFoo = (List)foo\n" + 812 | "return castFoo.join('')\n"); 813 | } 814 | 815 | @Issue("JENKINS-50470") 816 | @Test public void testCollectionGetProperty() throws Exception { 817 | assertIntercept(Arrays.asList( 818 | "new SimpleNamedBean(String)", 819 | "new SimpleNamedBean(String)", 820 | "new SimpleNamedBean(String)", 821 | // Before the JENKINS-50470 fix, this would just be ArrayList.name 822 | "SimpleNamedBean.name", 823 | "SimpleNamedBean.name", 824 | "SimpleNamedBean.name", 825 | "ArrayList.class", 826 | "ArrayList.join(String)", 827 | "String.plus(String)", 828 | "String.plus(Class)"), 829 | "abc class java.util.ArrayList", 830 | "def l = [new SimpleNamedBean('a'), new SimpleNamedBean('b'), new SimpleNamedBean('c')]\n" + 831 | "def nameList = l.name\n" + 832 | "def cl = l.class\n" + 833 | "return nameList.join('') + ' ' + cl\n"); 834 | } 835 | } 836 | -------------------------------------------------------------------------------- /src/main/java/org/kohsuke/groovy/sandbox/SandboxTransformer.java: -------------------------------------------------------------------------------- 1 | package org.kohsuke.groovy.sandbox; 2 | 3 | import java.lang.reflect.Modifier; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.concurrent.atomic.AtomicReference; 10 | import org.codehaus.groovy.ast.ASTNode; 11 | import org.codehaus.groovy.ast.ClassCodeExpressionTransformer; 12 | import org.codehaus.groovy.ast.ClassHelper; 13 | import org.codehaus.groovy.ast.ClassNode; 14 | import org.codehaus.groovy.ast.ConstructorNode; 15 | import org.codehaus.groovy.ast.FieldNode; 16 | import org.codehaus.groovy.ast.MethodNode; 17 | import org.codehaus.groovy.ast.Parameter; 18 | import org.codehaus.groovy.ast.VariableScope; 19 | import org.codehaus.groovy.ast.expr.ArgumentListExpression; 20 | import org.codehaus.groovy.ast.expr.ArrayExpression; 21 | import org.codehaus.groovy.ast.expr.AttributeExpression; 22 | import org.codehaus.groovy.ast.expr.BitwiseNegationExpression; 23 | import org.codehaus.groovy.ast.expr.CastExpression; 24 | import org.codehaus.groovy.ast.expr.ClassExpression; 25 | import org.codehaus.groovy.ast.expr.ClosureExpression; 26 | import org.codehaus.groovy.ast.expr.ConstantExpression; 27 | import org.codehaus.groovy.ast.expr.DeclarationExpression; 28 | import org.codehaus.groovy.ast.expr.EmptyExpression; 29 | import org.codehaus.groovy.ast.expr.Expression; 30 | import org.codehaus.groovy.ast.expr.ListExpression; 31 | import org.codehaus.groovy.ast.expr.MethodCallExpression; 32 | import org.codehaus.groovy.ast.expr.MethodPointerExpression; 33 | import org.codehaus.groovy.ast.expr.PostfixExpression; 34 | import org.codehaus.groovy.ast.expr.PrefixExpression; 35 | import org.codehaus.groovy.ast.expr.PropertyExpression; 36 | import org.codehaus.groovy.ast.expr.RangeExpression; 37 | import org.codehaus.groovy.ast.expr.StaticMethodCallExpression; 38 | import org.codehaus.groovy.ast.expr.TupleExpression; 39 | import org.codehaus.groovy.ast.expr.UnaryMinusExpression; 40 | import org.codehaus.groovy.ast.expr.UnaryPlusExpression; 41 | import org.codehaus.groovy.classgen.GeneratorContext; 42 | import org.codehaus.groovy.control.CompilePhase; 43 | import org.codehaus.groovy.control.SourceUnit; 44 | import org.codehaus.groovy.control.customizers.CompilationCustomizer; 45 | import org.codehaus.groovy.ast.expr.ConstructorCallExpression; 46 | import org.codehaus.groovy.ast.expr.BinaryExpression; 47 | import org.codehaus.groovy.runtime.ScriptBytecodeAdapter; 48 | import org.codehaus.groovy.syntax.Token; 49 | import org.codehaus.groovy.syntax.Types; 50 | import org.codehaus.groovy.ast.expr.FieldExpression; 51 | import org.codehaus.groovy.ast.expr.VariableExpression; 52 | import org.kohsuke.groovy.sandbox.impl.Checker; 53 | import org.kohsuke.groovy.sandbox.impl.Ops; 54 | import org.kohsuke.groovy.sandbox.impl.SandboxedMethodClosure; 55 | 56 | import static org.codehaus.groovy.ast.expr.ArgumentListExpression.EMPTY_ARGUMENTS; 57 | import org.codehaus.groovy.ast.stmt.BlockStatement; 58 | import org.codehaus.groovy.ast.stmt.ExpressionStatement; 59 | import org.codehaus.groovy.ast.stmt.ReturnStatement; 60 | import org.codehaus.groovy.ast.stmt.Statement; 61 | import org.codehaus.groovy.classgen.ReturnAdder; 62 | import org.codehaus.groovy.classgen.VariableScopeVisitor; 63 | import org.codehaus.groovy.classgen.Verifier; 64 | import static org.codehaus.groovy.syntax.Types.*; 65 | 66 | /** 67 | * Transforms Groovy code at compile-time to intercept when the script interacts with the outside world. 68 | * 69 | *

70 | * Sometimes you'd like to run Groovy scripts in a sandbox environment, where you only want it to 71 | * access limited subset of the rest of JVM. This transformation makes that possible by letting you inspect 72 | * every step of the script execution when it makes method calls and property/field/array access. 73 | * 74 | *

75 | * Once the script is transformed, every intercepted operation results in a call to {@link Checker}, 76 | * which further forwards the call to {@link GroovyInterceptor} for inspection. 77 | * 78 | * 79 | *

80 | * To use it, add it to the {@link org.codehaus.groovy.control.CompilerConfiguration}, like this: 81 | * 82 | *

  83 |  * def cc = new CompilerConfiguration()
  84 |  * cc.addCompilationCustomizers(new SandboxTransformer())
  85 |  * sh = new GroovyShell(cc)
  86 |  * 
87 | * 88 | *

89 | * By default, this code intercepts everything that can be intercepted, which are: 90 | *

    91 | *
  • Method calls (instance method and static method) 92 | *
  • Object allocation (that is, a constructor call except of the form "this(...)" and "super(...)") 93 | *
  • Property access (e.g., z=foo.bar, z=foo."bar") and assignment (e.g., foo.bar=z, foo."bar"=z) 94 | *
  • Attribute access (e.g., z=foo.@bar) and assignments (e.g., foo.@bar=z) 95 | *
  • Array access and assignment (z=x[y] and x[y]=z) 96 | *
97 | *

98 | * You can disable interceptions selectively by setting respective {@code interceptXXX} flags to {@code false}. 99 | * 100 | *

101 | * There'll be a substantial hit to the performance of the execution. 102 | * 103 | * @author Kohsuke Kawaguchi 104 | */ 105 | public class SandboxTransformer extends CompilationCustomizer { 106 | /** 107 | * Intercept method calls 108 | */ 109 | boolean interceptMethodCall=true; 110 | /** 111 | * Intercept object instantiation by intercepting its constructor call. 112 | * 113 | * Note that Java byte code doesn't allow the interception of super(...) and this(...) 114 | * so the object instantiation by defining and instantiating a subtype cannot be intercepted. 115 | */ 116 | boolean interceptConstructor=true; 117 | /** 118 | * Intercept property access for both read "(...).y" and write "(...).y=..." 119 | */ 120 | boolean interceptProperty=true; 121 | /** 122 | * Intercept array access for both read "y=a[x]" and write "a[x]=y" 123 | */ 124 | boolean interceptArray=true; 125 | /** 126 | * Intercept attribute access for both read "z=x.@y" and write "x.@y=z" 127 | */ 128 | boolean interceptAttribute=true; 129 | 130 | public SandboxTransformer() { 131 | super(CompilePhase.CANONICALIZATION); 132 | } 133 | 134 | @Override 135 | public void call(final SourceUnit source, GeneratorContext context, ClassNode classNode) { 136 | if (classNode == null) { // TODO is this even possible? CpsTransformer implies it is not. 137 | return; 138 | } 139 | 140 | // Removes all initial expressions for constructors and methods and generates overloads for all variants. 141 | new InitialExpressionExpander().expandInitialExpressions(source, classNode); 142 | 143 | ClassCodeExpressionTransformer visitor = createVisitor(source, classNode); 144 | 145 | processConstructors(visitor, classNode); 146 | for (MethodNode m : classNode.getMethods()) { 147 | forbidIfFinalizer(m); 148 | visitor.visitMethod(m); 149 | } 150 | for (Statement s : classNode.getObjectInitializerStatements()) { 151 | s.visit(visitor); 152 | } 153 | for (FieldNode f : classNode.getFields()) { 154 | visitor.visitField(f); 155 | } 156 | } 157 | 158 | /** 159 | * {@link Object#finalize} is called by the JVM outside of the sandbox, so overriding it in a 160 | * sandboxed script is not allowed. 161 | */ 162 | public void forbidIfFinalizer(MethodNode m) { 163 | if (m.getName().equals("finalize") && m.isVoidMethod() && !m.isPrivate() && !m.isStatic()) { 164 | boolean safe = false; 165 | /* 166 | Groovy allows method definitions to specify default arguments for parameters. Parameters with default 167 | arguments may be omitted when calling the method. Groovy implements this by generating additional 168 | overloaded methods in bytecode for each variation of the method being omitted. 169 | For example, given the following method: 170 | 171 | public void finalize(int x = 0) { } 172 | 173 | Groovy will generate 2 methods in bytecode: 174 | 175 | public void finalize(int x) { } 176 | public void finalize() { finalize(0) } 177 | 178 | Our AST transformer will not see the generated no-arg method which overrides Object#finalize, so we need 179 | to account for it by ensuring that at least one parameter does not have a default argument (AKA initial 180 | expression) for the method to be acceptable, because parameters without default arguments will exist in all 181 | generated methods. 182 | */ 183 | for (Parameter p : m.getParameters()) { 184 | if (!p.hasInitialExpression()) { 185 | safe = true; 186 | break; 187 | } 188 | } 189 | if (!safe) { 190 | throw new SecurityException("Sandboxed code may not override Object.finalize()"); 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * Do not care about {@code super} calls for classes extending these types. 197 | * 198 | *

Entries in this list must not have any constructors with parameters whose types are not safe to construct in 199 | * the sandbox, and they must be in a package that cannot be used to define new classes in the sandbox. 200 | */ 201 | private static final Set TRIVIAL_CONSTRUCTORS = Collections.singleton(Object.class.getName()); 202 | 203 | /** 204 | * Apply SECURITY-582 (and part of SECURITY-1754) fix to constructors. 205 | * 206 | * For example, given code like this: 207 | *

{@code
 208 |      * class B { }
 209 |      * class A extends B {
 210 |      *     A(T1 p1, ..., TM pM) {
 211 |      *         super(U1 a1, ..., UN aN) // or `this(...)`
 212 |      *         ...
 213 |      *     }
 214 |      * }
 215 |      * }
216 | * 217 | * {@link #processConstructors} will transform it into something like this: 218 | * 219 | *
{@code
 220 |      * class B { }
 221 |      * class A extends B {
 222 |      *     A(T1 p1, ..., TM pM) {
 223 |      *         this(Checker.checkedSuperConstructor( // or `Checker.checkedThisConstructor`
 224 |      *                 B.class,
 225 |      *                 new Object[]{a1, ..., aN},
 226 |      *                 new Object[]{p1, ..., pM},
 227 |      *                 new Class[]{SuperConstructorWrapper.class, T1.class, ..., TM.class}), // or `ThisConstructorWrapper.class`
 228 |      *             p1, ..., pM)
 229 |      *     }
 230 |      *     A(Checker.SuperConstructorWrapper $cw, T1 p1, ..., TM pM) { // Or `Checker.ThisConstructorWrapper $cw`
 231 |      *         super($cw.arg(1), ... cw.arg(N)) // or `this(...)`
 232 |      *         ...
 233 |      *     }
 234 |      * }
 235 |      * }
236 | */ 237 | public void processConstructors(final ClassCodeExpressionTransformer visitor, ClassNode classNode) { 238 | ClassNode superClass = classNode.getSuperClass(); 239 | List declaredConstructors = classNode.getDeclaredConstructors(); 240 | if (declaredConstructors.isEmpty()) { 241 | if (classNode.isInterface()) { 242 | // Interfaces are expected to not have constructors. 243 | return; 244 | } 245 | // Default constructor should have already been added by InitialExpressionExpander 246 | throw new AssertionError("No constructors for " + classNode); 247 | } else { 248 | declaredConstructors = new ArrayList<>(declaredConstructors); 249 | } 250 | for (ConstructorNode c : declaredConstructors) { 251 | for (Parameter p : c.getParameters()) { 252 | if (p.hasInitialExpression()) { 253 | // All initial expressions should have already been removed by InitialExpressionExpander 254 | throw new AssertionError("Found unexpected initial expression: " + p.getInitialExpression()); 255 | } 256 | } 257 | Statement code = c.getCode(); 258 | List body; 259 | if (code instanceof BlockStatement) { 260 | body = ((BlockStatement) code).getStatements(); 261 | } else { 262 | body = Collections.singletonList(code); 263 | } 264 | ClassNode constructorCallType = ClassNode.SUPER; 265 | TupleExpression constructorCallArgs = new TupleExpression(); 266 | if (!body.isEmpty() && body.get(0) instanceof ExpressionStatement && ((ExpressionStatement) body.get(0)).getExpression() instanceof ConstructorCallExpression) { 267 | ConstructorCallExpression cce = (ConstructorCallExpression) ((ExpressionStatement) body.get(0)).getExpression(); 268 | if (cce.isThisCall()) { 269 | constructorCallType = ClassNode.THIS; 270 | body = body.subList(1, body.size()); 271 | constructorCallArgs = ((TupleExpression) cce.getArguments()); 272 | } else if (cce.isSuperCall()) { 273 | body = body.subList(1, body.size()); 274 | constructorCallArgs = ((TupleExpression) cce.getArguments()); 275 | } else { 276 | // Some other class, for example if `new String();` happens to be the first statement 277 | // in a constructor. We handle this the same as an explicit call to `super()`. 278 | } 279 | } 280 | // SECURITY-3341 281 | // Only sandbox-transform super() constructor calls if the parent class is nontrivial. Always sandbox-transform this() constructor calls. 282 | if (constructorCallType == ClassNode.SUPER && TRIVIAL_CONSTRUCTORS.contains(superClass.getName())) { 283 | visitor.visitMethod(c); 284 | continue; 285 | } 286 | final TupleExpression _constructorCallArgs = constructorCallArgs; 287 | final AtomicReference constructorCallArgsTransformed = new AtomicReference<>(); 288 | ((ScopeTrackingClassCodeExpressionTransformer) visitor).withMethod(c, new Runnable() { 289 | @Override 290 | public void run() { 291 | constructorCallArgsTransformed.set(((VisitorImpl) visitor).transformArguments(_constructorCallArgs)); 292 | } 293 | }); 294 | // Create parameters for new constructor. 295 | Parameter[] origParams = c.getParameters(); 296 | Parameter[] params = new Parameter[origParams.length + 1]; 297 | params[0] = new Parameter(new ClassNode(constructorCallType == ClassNode.THIS ? Checker.ThisConstructorWrapper.class : Checker.SuperConstructorWrapper.class), "$cw"); 298 | System.arraycopy(origParams, 0, params, 1, origParams.length); 299 | List paramTypes = new ArrayList<>(params.length); 300 | for (Parameter p : params) { 301 | paramTypes.add(new ClassExpression(p.getType())); 302 | } 303 | // Create arguments for call to synthetic constructor. 304 | List thisArgs = new ArrayList<>(origParams.length + 1); 305 | thisArgs.add(null); // Placeholder 306 | List thisArgsWithoutWrapper = new ArrayList<>(origParams.length); 307 | for (Parameter p : origParams) { 308 | if (p.getType().equals(superConstructorWrapperClass) || p.getType().equals(thisConstructorWrapperClass)) { 309 | throw new SecurityException("Illegal constructor parameter for " + classNode + ": " + p); 310 | } 311 | thisArgs.add(new VariableExpression(p)); 312 | thisArgsWithoutWrapper.add(new VariableExpression(p)); 313 | } 314 | if (constructorCallType == ClassNode.THIS) { 315 | thisArgs.set(0, ((VisitorImpl) visitor).makeCheckedCall("checkedThisConstructor", 316 | new ClassExpression(classNode), 317 | constructorCallArgsTransformed.get(), 318 | new ArrayExpression(new ClassNode(Object.class), thisArgsWithoutWrapper), 319 | new ArrayExpression(new ClassNode(Class.class), paramTypes))); 320 | } else { 321 | thisArgs.set(0, ((VisitorImpl) visitor).makeCheckedCall("checkedSuperConstructor", 322 | new ClassExpression(classNode), 323 | new ClassExpression(superClass), 324 | constructorCallArgsTransformed.get(), 325 | new ArrayExpression(new ClassNode(Object.class), thisArgsWithoutWrapper), 326 | new ArrayExpression(new ClassNode(Class.class), paramTypes))); 327 | } 328 | c.setCode(new BlockStatement(new Statement[] {new ExpressionStatement(new ConstructorCallExpression(ClassNode.THIS, new TupleExpression(thisArgs)))}, c.getVariableScope())); 329 | List cwArgs = new ArrayList<>(); 330 | int x = 0; 331 | for (Expression constructorCallArg : constructorCallArgs) { 332 | cwArgs.add(/*new CastExpression(superArg.getType(), */new MethodCallExpression(new VariableExpression("$cw"), "arg", new ConstantExpression(x++))/*)*/); 333 | } 334 | List body2 = new ArrayList<>(body.size() + 1); 335 | body2.add(0, new ExpressionStatement(new ConstructorCallExpression(constructorCallType, new ArgumentListExpression(cwArgs)))); 336 | body2.addAll(body); 337 | ((ScopeTrackingClassCodeExpressionTransformer) visitor).withMethod(c, () -> { 338 | for (int i = 1; i < body2.size(); i++) { // Skip the first statement, which is the constructor call. 339 | body2.get(i).visit(visitor); 340 | } 341 | }); 342 | final int SYNTHETIC = 0x00001000; // Not public in Modifier 343 | ConstructorNode c2 = new ConstructorNode(Modifier.PRIVATE | SYNTHETIC, params, c.getExceptions(), new BlockStatement(body2, c.getVariableScope())); 344 | // perhaps more misleading than helpful: c2.setSourcePosition(c); 345 | classNode.addConstructor(c2); 346 | } 347 | } 348 | 349 | @Deprecated 350 | public ClassCodeExpressionTransformer createVisitor(SourceUnit source) { 351 | return createVisitor(source, null); 352 | } 353 | 354 | public ClassCodeExpressionTransformer createVisitor(SourceUnit source, ClassNode clazz) { 355 | return new VisitorImpl(source, clazz); 356 | } 357 | 358 | class VisitorImpl extends ScopeTrackingClassCodeExpressionTransformer { 359 | private final SourceUnit sourceUnit; 360 | /** 361 | * Invocation/property access without the left-hand side expression (for example {@code foo()} 362 | * as opposed to {@code something.foo()} means {@code this.foo()} in Java, but this is not 363 | * so in Groovy. 364 | * 365 | * In Groovy, {@code foo()} inside a closure uses the closure object itself as the lhs value, 366 | * whereas {@code this} in closure refers to a nearest enclosing non-closure object. 367 | * 368 | * So we cannot always expand {@code foo()} to {@code this.foo()}. 369 | * 370 | * To keep track of when we can expand {@code foo()} to {@code this.foo()} and when we can't, 371 | * we maintain this flag as we visit the expression tree. This flag is set to true 372 | * while we are visiting the body of the closure (the part between { ... }), and switched 373 | * back to false as we visit inner classes. 374 | * 375 | * To correctly expand {@code foo()} in the closure requires an access to the closure object itself, 376 | * and unfortunately Groovy doesn't seem to have any reliable way to do this. The hack I came up 377 | * with is {@code asWritable().getOwner()}, but even that is subject to the method resolution rule. 378 | * 379 | */ 380 | private boolean visitingClosureBody; 381 | 382 | /** 383 | * Current class we are traversing. 384 | */ 385 | private ClassNode clazz; 386 | 387 | /** 388 | * Return type of the current method or closure body that we are traversing. 389 | */ 390 | private ClassNode methodReturnType; 391 | 392 | VisitorImpl(SourceUnit sourceUnit, ClassNode clazz) { 393 | this.sourceUnit = sourceUnit; 394 | this.clazz = clazz; 395 | } 396 | 397 | @Override 398 | public void visitMethod(MethodNode node) { 399 | if (clazz == null) { // compatibility 400 | clazz = node.getDeclaringClass(); 401 | } 402 | methodReturnType = node.getReturnType(); 403 | try { 404 | // Add explicit return statements so we can insert casts as needed. 405 | ReturnAdder adder = new ReturnAdder(); 406 | adder.visitMethod(node); 407 | super.visitMethod(node); 408 | } finally { 409 | methodReturnType = null; 410 | } 411 | } 412 | 413 | @Override 414 | public void visitReturnStatement(ReturnStatement statement) { 415 | if (statement.isReturningNullOrVoid()) { 416 | // We must not mutate ReturnStatement.RETURN_NULL_OR_VOID, and we don't care about casting null anyway. 417 | return; 418 | } 419 | super.visitReturnStatement(statement); 420 | // statement.getExpression has already been transformed by the super call, so we do not transform it twice. 421 | statement.setExpression(makeCheckedGroovyCast(methodReturnType, statement.getExpression())); 422 | } 423 | 424 | @Override 425 | public void visitField(FieldNode node) { 426 | super.visitField(node); 427 | // When using @Field with a declaration that has no default value, node.getInitialExpression is an 428 | // EmptyExpression rather than null, so we must ignore it to avoid breaking things. 429 | if (node.hasInitialExpression() && !(node.getInitialValueExpression() instanceof EmptyExpression)) { 430 | // node.getInitialValueExpression has already been transformed by the super call, so we do not transform it twice. 431 | node.setInitialValueExpression(makeCheckedGroovyCast(node.getType(), node.getInitialValueExpression())); 432 | } 433 | } 434 | 435 | /** 436 | * Transforms the arguments of a call. 437 | * Groovy primarily uses {@link ArgumentListExpression} for this, 438 | * but the signature doesn't guarantee that. So this method takes care of that. 439 | */ 440 | Expression transformArguments(Expression e) { 441 | List l; 442 | if (e instanceof TupleExpression) { 443 | List expressions = ((TupleExpression) e).getExpressions(); 444 | l = new ArrayList<>(expressions.size()); 445 | for (Expression expression : expressions) { 446 | l.add(transform(expression)); 447 | } 448 | } else { 449 | l = Collections.singletonList(transform(e)); 450 | } 451 | 452 | // checkdCall expects an array 453 | return withLoc(e,new MethodCallExpression(new ListExpression(l),"toArray",new ArgumentListExpression())); 454 | } 455 | 456 | Expression makeCheckedCall(String name, Expression... arguments) { 457 | return new StaticMethodCallExpression(checkerClass,name, 458 | new ArgumentListExpression(arguments)); 459 | } 460 | 461 | /** 462 | * Groovy implicitly casts some expressions at runtime, so we manually insert explicit casts as needed to 463 | * intercept potentially dangerous calls. 464 | */ 465 | Expression makeCheckedGroovyCast(ClassNode clazz, Expression value) { 466 | if (isKnownSafeCast(clazz, value)) { 467 | return value; 468 | } 469 | return makeCheckedCall("checkedCast", 470 | classExp(clazz), 471 | value, 472 | boolExp(false), 473 | boolExp(false), // Groovy evaluates implicit casts using ScriptByteCodeAdapter.castToType, so coerce must be false. 474 | boolExp(false)); 475 | } 476 | 477 | @Override 478 | public Expression transform(Expression exp) { 479 | Expression o = innerTransform(exp); 480 | if (o!=exp) { 481 | o.setSourcePosition(exp); 482 | } 483 | return o; 484 | } 485 | 486 | private Expression innerTransform(Expression exp) { 487 | if (exp instanceof ClosureExpression) { 488 | // ClosureExpression.transformExpression doesn't visit the code inside 489 | ClosureExpression ce = (ClosureExpression)exp; 490 | try (StackVariableSet scope = new StackVariableSet(this)) { 491 | Parameter[] parameters = ce.getParameters(); 492 | if (parameters != null) { 493 | // Explicitly defined parameters, i.e., ".findAll { i -> i == 'bar' }" 494 | if (parameters.length > 0) { 495 | for (Parameter p : parameters) { 496 | if (p.hasInitialExpression()) { 497 | Expression init = p.getInitialExpression(); 498 | p.setInitialExpression(makeCheckedGroovyCast(p.getType(), transform(init))); 499 | } 500 | } 501 | for (Parameter p : parameters) { 502 | declareVariable(p); 503 | } 504 | } else { 505 | // Implicit parameter - i.e., ".findAll { it == 'bar' }" 506 | declareVariable(new Parameter(ClassHelper.DYNAMIC_TYPE, "it")); 507 | } 508 | } 509 | boolean old = visitingClosureBody; 510 | visitingClosureBody = true; 511 | ClassNode oldMethodReturnType = methodReturnType; 512 | methodReturnType = ClassHelper.OBJECT_TYPE; 513 | try { 514 | ce.getCode().visit(this); 515 | } finally { 516 | visitingClosureBody = old; 517 | methodReturnType = oldMethodReturnType; 518 | } 519 | } 520 | } 521 | 522 | if (exp instanceof MethodCallExpression && interceptMethodCall) { 523 | // lhs.foo(arg1,arg2) => checkedCall(lhs,"foo",arg1,arg2) 524 | // lhs+rhs => lhs.plus(rhs) 525 | // Integer.plus(Integer) => DefaultGroovyMethods.plus 526 | // lhs || rhs => lhs.or(rhs) 527 | MethodCallExpression call = (MethodCallExpression) exp; 528 | 529 | Expression objExp; 530 | if (call.isImplicitThis() && visitingClosureBody && !isLocalVariableExpression(call.getObjectExpression())) 531 | objExp = CLOSURE_THIS; 532 | else 533 | objExp = transform(call.getObjectExpression()); 534 | 535 | Expression arg1 = transform(call.getMethod()); 536 | Expression arg2 = transformArguments(call.getArguments()); 537 | 538 | if (call.getObjectExpression() instanceof VariableExpression && ((VariableExpression) call.getObjectExpression()).getName().equals("super")) { 539 | if (clazz == null) { 540 | throw new IllegalStateException("owning class not defined"); 541 | } 542 | return makeCheckedCall("checkedSuperCall", new ClassExpression(clazz), objExp, arg1, arg2); 543 | } else { 544 | return makeCheckedCall("checkedCall", 545 | objExp, 546 | boolExp(call.isSafe()), 547 | boolExp(call.isSpreadSafe()), 548 | arg1, 549 | arg2); 550 | } 551 | } 552 | 553 | if (exp instanceof StaticMethodCallExpression && interceptMethodCall) { 554 | /* 555 | Groovy doesn't use StaticMethodCallExpression as much as it could in compilation. 556 | For example, "Math.max(1,2)" results in a regular MethodCallExpression. 557 | 558 | Static import handling uses StaticMethodCallExpression, and so are some 559 | ASTTransformations like ToString,EqualsAndHashCode, etc. 560 | */ 561 | StaticMethodCallExpression call = (StaticMethodCallExpression) exp; 562 | return makeCheckedCall("checkedStaticCall", 563 | new ClassExpression(call.getOwnerType()), 564 | new ConstantExpression(call.getMethod()), 565 | transformArguments(call.getArguments()) 566 | ); 567 | } 568 | 569 | if (exp instanceof MethodPointerExpression && interceptMethodCall) { 570 | MethodPointerExpression mpe = (MethodPointerExpression) exp; 571 | return new ConstructorCallExpression( 572 | new ClassNode(SandboxedMethodClosure.class), 573 | new ArgumentListExpression( 574 | transform(mpe.getExpression()), 575 | transform(mpe.getMethodName())) 576 | ); 577 | } 578 | 579 | if (exp instanceof ConstructorCallExpression && interceptConstructor) { 580 | if (!((ConstructorCallExpression) exp).isSpecialCall()) { 581 | // creating a new instance, like "new Foo(...)" 582 | return makeCheckedCall("checkedConstructor", 583 | new ClassExpression(exp.getType()), 584 | transformArguments(((ConstructorCallExpression) exp).getArguments()) 585 | ); 586 | } else { 587 | // we can't really intercept constructor calling super(...) or this(...), 588 | // since it has to be the first method call in a constructor. 589 | // but see SECURITY-582 fix above 590 | } 591 | } 592 | 593 | if (exp instanceof AttributeExpression && interceptAttribute) { 594 | AttributeExpression ae = (AttributeExpression) exp; 595 | return makeCheckedCall("checkedGetAttribute", 596 | transform(ae.getObjectExpression()), 597 | boolExp(ae.isSafe()), 598 | boolExp(ae.isSpreadSafe()), 599 | transform(ae.getProperty()) 600 | ); 601 | } 602 | 603 | if (exp instanceof PropertyExpression && interceptProperty) { 604 | PropertyExpression pe = (PropertyExpression) exp; 605 | return makeCheckedCall("checkedGetProperty", 606 | transformObjectExpression(pe), 607 | boolExp(pe.isSafe()), 608 | boolExp(pe.isSpreadSafe()), 609 | transform(pe.getProperty()) 610 | ); 611 | } 612 | 613 | if (exp instanceof FieldExpression && interceptProperty) { 614 | // I am not sure whether this is reachable. See note below regarding the only known case of FieldExpression in the AST. 615 | FieldExpression fe = (FieldExpression) exp; 616 | return makeCheckedCall("checkedGetAttribute", 617 | new VariableExpression("this"), 618 | boolExp(false), 619 | boolExp(false), 620 | stringExp(fe.getFieldName()) 621 | ); 622 | } 623 | 624 | if (exp instanceof VariableExpression && interceptProperty) { 625 | VariableExpression vexp = (VariableExpression) exp; 626 | if (isLocalVariable(vexp.getName()) || vexp.getName().equals("this") || vexp.getName().equals("super")) { 627 | // We don't care what sandboxed code does to itself until it starts interacting with outside world 628 | return super.transform(exp); 629 | } else { 630 | // if the variable is not in-scope local variable, it gets treated as a property access with implicit this. 631 | // see AsmClassGenerator.visitVariableExpression and processClassVariable. 632 | PropertyExpression pexp = new PropertyExpression(VariableExpression.THIS_EXPRESSION, vexp.getName()); 633 | pexp.setImplicitThis(true); 634 | withLoc(exp,pexp); 635 | return transform(pexp); 636 | } 637 | } 638 | 639 | if (exp instanceof DeclarationExpression) { 640 | handleDeclarations((DeclarationExpression) exp); 641 | // We handle DeclarationExpression here to simplify handling of BinaryExpression for non-declaration assignments. 642 | DeclarationExpression de = (DeclarationExpression) exp; 643 | Expression rhs = de.getRightExpression(); 644 | if (rhs instanceof EmptyExpression) { 645 | // Declaration without initialization. 646 | return exp; 647 | } else if (de.isMultipleAssignmentDeclaration()) { 648 | throw new UnsupportedOperationException("The sandbox does not currently support multiple assignment"); 649 | } 650 | return withLoc(de, new DeclarationExpression(de.getVariableExpression(), de.getOperation(), 651 | makeCheckedGroovyCast(de.getVariableExpression().getType(), transform(rhs)))); 652 | } 653 | 654 | if (exp instanceof BinaryExpression) { 655 | BinaryExpression be = (BinaryExpression) exp; 656 | // this covers everything from a+b to a=b 657 | if (ofType(be.getOperation().getType(),ASSIGNMENT_OPERATOR)) { 658 | // simple assignment like '=' as well as compound assignments like "+=","-=", etc. 659 | 660 | // How we dispatch this depends on the type of left expression. 661 | // 662 | // What can be LHS? 663 | // according to AsmClassGenerator, PropertyExpression, AttributeExpression, FieldExpression, VariableExpression 664 | // Can also be TupleExpression, but we do not currently handle that. 665 | 666 | Expression lhs = be.getLeftExpression(); 667 | if (lhs instanceof VariableExpression) { 668 | VariableExpression vexp = (VariableExpression) lhs; 669 | if (isLocalVariable(vexp.getName()) || vexp.getName().equals("this") || vexp.getName().equals("super")) { 670 | return withLoc(be, new BinaryExpression(lhs, be.getOperation(), 671 | makeCheckedGroovyCast(vexp.getType(), transform(be.getRightExpression())))); 672 | } else { 673 | // if the variable is not in-scope local variable, it gets treated as a property access with implicit this. 674 | // see AsmClassGenerator.visitVariableExpression and processClassVariable. 675 | PropertyExpression pexp = new PropertyExpression(VariableExpression.THIS_EXPRESSION, vexp.getName()); 676 | pexp.setImplicitThis(true); 677 | pexp.setSourcePosition(vexp); 678 | 679 | lhs = pexp; 680 | } 681 | } // no else here 682 | if (lhs instanceof PropertyExpression) { 683 | PropertyExpression pe = (PropertyExpression) lhs; 684 | String name = null; 685 | if (lhs instanceof AttributeExpression) { 686 | if (interceptAttribute) 687 | name = "checkedSetAttribute"; 688 | } else { 689 | Expression receiver = pe.getObjectExpression(); 690 | if (receiver instanceof VariableExpression && ((VariableExpression) receiver).isThisExpression()) { 691 | FieldNode field = clazz != null ? clazz.getField(pe.getPropertyAsString()) : null; 692 | if (field != null) { 693 | // "this.x = y" must be handled specially to prevent the sandbox from using 694 | // reflection to assign values to final fields in constructors and initializers 695 | // and to prevent infinite loops in setter methods. 696 | Token op = be.getOperation(); 697 | if (op.getType() == Types.ASSIGN) { 698 | return new BinaryExpression(new FieldExpression(field), op, 699 | makeCheckedGroovyCast(field.getType(), transform(be.getRightExpression()))); 700 | } else { 701 | // Groovy does not support FieldExpression with compound assignment operators 702 | // directly, so we must expand the expression ourselves. 703 | Token plainAssignment = Token.newSymbol(Types.ASSIGN, op.getStartLine(), op.getStartColumn()); 704 | return new BinaryExpression(new FieldExpression(field), plainAssignment, 705 | makeCheckedGroovyCast(field.getType(), 706 | makeCheckedCall("checkedBinaryOp", 707 | new FieldExpression(field), 708 | intExp(Ops.compoundAssignmentToBinaryOperator(op.getType())), 709 | transform(be.getRightExpression())))); 710 | } 711 | } // else this is a property which we need to check 712 | } 713 | if (interceptProperty) 714 | name = "checkedSetProperty"; 715 | } 716 | if (name==null) // not intercepting? 717 | return super.transform(exp); 718 | 719 | return makeCheckedCall(name, 720 | transformObjectExpression(pe), 721 | transform(pe.getProperty()), 722 | boolExp(pe.isSafe()), 723 | boolExp(pe.isSpreadSafe()), 724 | intExp(be.getOperation().getType()), 725 | transform(be.getRightExpression()) 726 | ); 727 | } else 728 | if (lhs instanceof FieldExpression) { 729 | // The only known occurrences of this expression in the AST are for the `this$0` field that is 730 | // added to anonymous and inner classes to allow them to access their outer class and for 731 | // assigning the values of static enum constant fields in synthetically generated enum constructors. 732 | FieldExpression fe = (FieldExpression) lhs; 733 | if (fe.getField().isFinal()) { 734 | // Assignments to final fields cannot be done reflectively, so we leave FieldExpression untransformed. 735 | return withLoc(be, new BinaryExpression(fe, be.getOperation(), 736 | makeCheckedGroovyCast(fe.getType(), transform(be.getRightExpression())))); 737 | } 738 | return withLoc(be, makeCheckedCall("checkedSetAttribute", 739 | new VariableExpression("this"), 740 | stringExp(fe.getFieldName()), 741 | boolExp(false), 742 | boolExp(false), 743 | intExp(be.getOperation().getType()), 744 | makeCheckedGroovyCast(fe.getType(), transform(be.getRightExpression())))); 745 | } else 746 | if (lhs instanceof BinaryExpression) { 747 | BinaryExpression lbe = (BinaryExpression) lhs; 748 | if (lbe.getOperation().getType()==Types.LEFT_SQUARE_BRACKET && interceptArray) {// expression of the form "x[y] = z" 749 | return makeCheckedCall("checkedSetArray", 750 | transform(lbe.getLeftExpression()), 751 | transform(lbe.getRightExpression()), 752 | intExp(be.getOperation().getType()), 753 | transform(be.getRightExpression()) 754 | ); 755 | } 756 | } else if (lhs instanceof TupleExpression) { 757 | throw new UnsupportedOperationException("The sandbox does not support multiple assignment"); 758 | } 759 | throw new AssertionError("Unexpected LHS of an assignment: " + lhs.getClass()); 760 | } 761 | if (be.getOperation().getType()==Types.LEFT_SQUARE_BRACKET) {// array reference 762 | if (interceptArray) 763 | return makeCheckedCall("checkedGetArray", 764 | transform(be.getLeftExpression()), 765 | transform(be.getRightExpression()) 766 | ); 767 | } else 768 | if (be.getOperation().getType()==Types.KEYWORD_INSTANCEOF) {// instanceof operator 769 | return super.transform(exp); 770 | } else 771 | if (Ops.isLogicalOperator(be.getOperation().getType())) { 772 | return super.transform(exp); 773 | } else 774 | if (be.getOperation().getType()==Types.KEYWORD_IN) {// membership operator: JENKINS-28154 775 | // This requires inverted operand order: 776 | // "a in [...]" -> "[...].isCase(a)" 777 | if (interceptMethodCall) 778 | return makeCheckedCall("checkedCall", 779 | transform(be.getRightExpression()), 780 | boolExp(false), 781 | boolExp(false), 782 | stringExp("isCase"), 783 | transform(be.getLeftExpression()) 784 | 785 | ); 786 | } else 787 | if (Ops.isRegexpComparisonOperator(be.getOperation().getType())) { 788 | if (interceptMethodCall) 789 | return makeCheckedCall("checkedStaticCall", 790 | classExp(ScriptBytecodeAdapterClass), 791 | stringExp(Ops.binaryOperatorMethods(be.getOperation().getType())), 792 | transform(be.getLeftExpression()), 793 | transform(be.getRightExpression()) 794 | ); 795 | } else 796 | if (Ops.isComparisionOperator(be.getOperation().getType())) { 797 | if (interceptMethodCall) { 798 | return makeCheckedCall("checkedComparison", 799 | transform(be.getLeftExpression()), 800 | intExp(be.getOperation().getType()), 801 | transform(be.getRightExpression()) 802 | ); 803 | } 804 | } else 805 | if (interceptMethodCall) { 806 | // normally binary operators like a+b 807 | // TODO: check what other weird binary operators land here 808 | return makeCheckedCall("checkedBinaryOp", 809 | transform(be.getLeftExpression()), 810 | intExp(be.getOperation().getType()), 811 | transform(be.getRightExpression()) 812 | ); 813 | } 814 | } 815 | 816 | if (exp instanceof PostfixExpression) { 817 | PostfixExpression pe = (PostfixExpression) exp; 818 | return prefixPostfixExp(exp, pe.getExpression(), pe.getOperation(), "Postfix"); 819 | } 820 | if (exp instanceof PrefixExpression) { 821 | PrefixExpression pe = (PrefixExpression) exp; 822 | return prefixPostfixExp(exp, pe.getExpression(), pe.getOperation(), "Prefix"); 823 | } 824 | 825 | if (exp instanceof CastExpression) { 826 | CastExpression ce = (CastExpression) exp; 827 | return makeCheckedCall("checkedCast", 828 | classExp(exp.getType()), 829 | transform(ce.getExpression()), 830 | boolExp(ce.isIgnoringAutoboxing()), 831 | boolExp(ce.isCoerce()), 832 | boolExp(ce.isStrict()) 833 | ); 834 | } 835 | 836 | if (exp instanceof BitwiseNegationExpression) { 837 | BitwiseNegationExpression bne = (BitwiseNegationExpression) exp; 838 | return makeCheckedCall("checkedBitwiseNegate", transform(bne.getExpression())); 839 | } 840 | 841 | if (exp instanceof RangeExpression) { 842 | RangeExpression re = (RangeExpression) exp; 843 | return makeCheckedCall("checkedCreateRange", 844 | transform(re.getFrom()), 845 | transform(re.getTo()), 846 | boolExp(re.isInclusive())); 847 | } 848 | 849 | if (exp instanceof UnaryMinusExpression) { 850 | UnaryMinusExpression ume = (UnaryMinusExpression) exp; 851 | return makeCheckedCall("checkedUnaryMinus", transform(ume.getExpression())); 852 | } 853 | 854 | if (exp instanceof UnaryPlusExpression) { 855 | UnaryPlusExpression upe = (UnaryPlusExpression) exp; 856 | return makeCheckedCall("checkedUnaryPlus", transform(upe.getExpression())); 857 | } 858 | 859 | return super.transform(exp); 860 | } 861 | 862 | // Handles the cases mentioned in BinaryExpressionHelper.execMethodAndStoreForSubscriptOperator: https://github.com/apache/groovy/blob/GROOVY_2_4_7/src/main/org/codehaus/groovy/classgen/asm/BinaryExpressionHelper.java#L695 863 | private Expression prefixPostfixExp(Expression whole, Expression atom, Token opToken, String mode) { 864 | String op = opToken.getText().equals("++") ? "next" : "previous"; 865 | 866 | // a[b]++ 867 | if (atom instanceof BinaryExpression && ((BinaryExpression) atom).getOperation().getType()==Types.LEFT_SQUARE_BRACKET && interceptArray) { 868 | return makeCheckedCall("checked" + mode + "Array", 869 | transform(((BinaryExpression) atom).getLeftExpression()), 870 | transform(((BinaryExpression) atom).getRightExpression()), 871 | stringExp(op) 872 | ); 873 | } 874 | 875 | // a++ 876 | if (atom instanceof VariableExpression) { 877 | VariableExpression ve = (VariableExpression) atom; 878 | if (isLocalVariable(ve.getName())) { 879 | if (mode.equals("Postfix")) { 880 | // a trick to rewrite a++ without introducing a new local variable 881 | // a++ -> [a,a=a.next()][0] 882 | return transform(withLoc(whole,new BinaryExpression( 883 | new ListExpression(Arrays.asList( 884 | atom, 885 | new BinaryExpression(atom, ASSIGNMENT_OP, 886 | withLoc(atom,new MethodCallExpression(atom,op,EMPTY_ARGUMENTS))) 887 | )), 888 | new Token(Types.LEFT_SQUARE_BRACKET, "[", -1,-1), 889 | new ConstantExpression(0) 890 | ))); 891 | } else { 892 | // ++a -> a=a.next() 893 | return transform(withLoc(whole,new BinaryExpression(atom,ASSIGNMENT_OP, 894 | withLoc(atom,new MethodCallExpression(atom,op,EMPTY_ARGUMENTS))) 895 | )); 896 | } 897 | } else { 898 | // if the variable is not in-scope local variable, it gets treated as a property access with implicit this. 899 | // see AsmClassGenerator.visitVariableExpression and processClassVariable. 900 | PropertyExpression pexp = new PropertyExpression(VariableExpression.THIS_EXPRESSION, ve.getName()); 901 | pexp.setImplicitThis(true); 902 | pexp.setSourcePosition(atom); 903 | 904 | atom = pexp; 905 | // fall through to the "a.b++" case below 906 | } 907 | } 908 | 909 | // a.@b++ 910 | if (atom instanceof AttributeExpression && interceptProperty) { 911 | AttributeExpression ae = (AttributeExpression) atom; 912 | return makeCheckedCall("checked" + mode + "Attribute", 913 | transformObjectExpression(ae), 914 | transform(ae.getProperty()), 915 | boolExp(ae.isSafe()), 916 | boolExp(ae.isSpreadSafe()), 917 | stringExp(op) 918 | ); 919 | } 920 | 921 | // a.b++ 922 | if (atom instanceof PropertyExpression && interceptProperty) { 923 | PropertyExpression pe = (PropertyExpression) atom; 924 | return makeCheckedCall("checked" + mode + "Property", 925 | transformObjectExpression(pe), 926 | transform(pe.getProperty()), 927 | boolExp(pe.isSafe()), 928 | boolExp(pe.isSpreadSafe()), 929 | stringExp(op) 930 | ); 931 | } 932 | 933 | // this.b++ where this.b is a FieldExpression. 934 | // It is unclear if this is actually reachable. I think that syntax like `this.b` will always be a 935 | // PropertyExpression in this context. We handle it explicitly as a precaution, since the catch-all 936 | // below does not store the result, which would definitely be wrong for FieldExpression. 937 | if (atom instanceof FieldExpression) { 938 | FieldExpression fe = (FieldExpression) atom; 939 | return makeCheckedCall("checked" + mode + "Attribute", 940 | new VariableExpression("this"), 941 | stringExp(fe.getFieldName()), 942 | boolExp(false), 943 | boolExp(false), 944 | stringExp(op) 945 | ); 946 | } 947 | 948 | // method()++, 1++, any other cases where "atom" is not valid as the LHS of an assignment expression, so no 949 | // store is performed, see https://github.com/apache/groovy/blob/GROOVY_2_4_7/src/main/org/codehaus/groovy/classgen/asm/BinaryExpressionHelper.java#L724. 950 | if (mode.equals("Postfix")) { 951 | // We need to intercept the call to x.next() while making sure that x is not evaluated more than once. 952 | // x++ -> (temp -> { temp.next(); temp }(x)) 953 | VariableScope closureScope = new VariableScope(); 954 | ClosureExpression closure = withLoc(whole, new ClosureExpression( 955 | new Parameter[] { new Parameter(ClassHelper.DYNAMIC_TYPE, "temp") }, 956 | new BlockStatement( 957 | Arrays.asList( 958 | new ExpressionStatement(new MethodCallExpression( 959 | new VariableExpression("temp"), op, EMPTY_ARGUMENTS)), 960 | new ExpressionStatement(new VariableExpression("temp"))), 961 | new VariableScope(closureScope)))); 962 | closure.setVariableScope(closureScope); 963 | return transform(withLoc(whole, 964 | new MethodCallExpression(closure, "call", new ArgumentListExpression(atom)))); 965 | } else { 966 | // ++x -> x.next() 967 | return transform(withLoc(whole, new MethodCallExpression(atom, op, EMPTY_ARGUMENTS))); 968 | } 969 | } 970 | 971 | /** 972 | * Decorates an {@link ASTNode} by copying source location from another node. 973 | */ 974 | private T withLoc(ASTNode src, T t) { 975 | t.setSourcePosition(src); 976 | return t; 977 | } 978 | 979 | /** 980 | * See {@link #visitingClosureBody} for the details of what this method is about. 981 | */ 982 | private Expression transformObjectExpression(PropertyExpression exp) { 983 | if (exp.isImplicitThis() && visitingClosureBody && !isLocalVariableExpression(exp.getObjectExpression())) { 984 | return CLOSURE_THIS; 985 | } else { 986 | return transform(exp.getObjectExpression()); 987 | } 988 | } 989 | 990 | private boolean isLocalVariableExpression(Expression exp) { 991 | if (exp != null && exp instanceof VariableExpression) { 992 | return isLocalVariable(((VariableExpression) exp).getName()); 993 | } 994 | 995 | return false; 996 | } 997 | 998 | ConstantExpression boolExp(boolean v) { 999 | return v ? ConstantExpression.PRIM_TRUE : ConstantExpression.PRIM_FALSE; 1000 | } 1001 | 1002 | ConstantExpression intExp(int v) { 1003 | return new ConstantExpression(v,true); 1004 | } 1005 | 1006 | ClassExpression classExp(ClassNode c) { 1007 | return new ClassExpression(c); 1008 | } 1009 | 1010 | ConstantExpression stringExp(String v) { 1011 | return new ConstantExpression(v); 1012 | } 1013 | 1014 | @Override 1015 | protected SourceUnit getSourceUnit() { 1016 | return sourceUnit; 1017 | } 1018 | } 1019 | 1020 | // Subclassing is required because the methods we need have protected visibility in Verifier. 1021 | public static class InitialExpressionExpander extends Verifier { 1022 | public void expandInitialExpressions(SourceUnit source, ClassNode node) { 1023 | super.setClassNode(node); 1024 | if (node.isInterface()) { 1025 | return; 1026 | } 1027 | super.addDefaultParameterMethods(node); 1028 | super.addDefaultParameterConstructors(node); 1029 | super.addDefaultConstructor(node); 1030 | // addDefaultParameterMethods introduces VariablesExpressions with a null getAccessedVariable(), so we 1031 | // rerun VariableScopeVisitor to prevent issues when this is used by groovy-cps. 1032 | new VariableScopeVisitor(source).visitClass(node); 1033 | } 1034 | } 1035 | 1036 | /** 1037 | * Return true if this cast is statically known to be safe and does not need to be checked at runtime. 1038 | * 1039 | * @see Checker#preCheckedCast 1040 | */ 1041 | public static boolean isKnownSafeCast(ClassNode type, Expression exp) { 1042 | if (exp.getType().isDerivedFrom(type) || exp.getType().implementsInterface(type)) { 1043 | return true; 1044 | } else if (exp instanceof ConstantExpression && ((ConstantExpression)exp).isNullExpression()) { 1045 | return true; 1046 | } else if (exp instanceof EmptyExpression) { 1047 | // If we get here, something has already gone wrong, and inserting a checked cast would make things even worse. 1048 | return true; 1049 | } 1050 | return false; 1051 | } 1052 | 1053 | static final Token ASSIGNMENT_OP = new Token(Types.ASSIGN, "=", -1, -1); 1054 | 1055 | static final ClassNode checkerClass = new ClassNode(Checker.class); 1056 | static final ClassNode ScriptBytecodeAdapterClass = new ClassNode(ScriptBytecodeAdapter.class); 1057 | static final ClassNode superConstructorWrapperClass = new ClassNode(Checker.SuperConstructorWrapper.class); 1058 | static final ClassNode thisConstructorWrapperClass = new ClassNode(Checker.ThisConstructorWrapper.class); 1059 | 1060 | /** 1061 | * Expression that accesses the closure object itself from within the closure. 1062 | * 1063 | * Currently a hacky "asWritable().getOwner()" 1064 | */ 1065 | static final Expression CLOSURE_THIS; 1066 | 1067 | static { 1068 | MethodCallExpression aw = new MethodCallExpression(new VariableExpression("this"),"asWritable",EMPTY_ARGUMENTS); 1069 | aw.setImplicitThis(true); 1070 | 1071 | CLOSURE_THIS = new MethodCallExpression(aw,"getOwner",EMPTY_ARGUMENTS); 1072 | } 1073 | } 1074 | --------------------------------------------------------------------------------