diff --git a/groovy-sandbox/LICENSE.md b/groovy-sandbox/LICENSE.md
new file mode 100644
index 000000000..6dbc195fb
--- /dev/null
+++ b/groovy-sandbox/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2012-2014 Kohsuke Kawaguchi, CloudBees, Inc., other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/groovy-sandbox/README.md b/groovy-sandbox/README.md
new file mode 100644
index 000000000..8fb9eb9c0
--- /dev/null
+++ b/groovy-sandbox/README.md
@@ -0,0 +1,8 @@
+groovy-sandbox
+==============
+
+**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.
+
+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.
+
+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.
diff --git a/groovy-sandbox/ast.sh b/groovy-sandbox/ast.sh
new file mode 100755
index 000000000..939fa8eed
--- /dev/null
+++ b/groovy-sandbox/ast.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+# show the AST tree of the specified Groovy file in GUI
+exec groovy -e 'groovy.inspect.swingui.AstBrowser.main(args)' "$@"
diff --git a/groovy-sandbox/pom.xml b/groovy-sandbox/pom.xml
new file mode 100644
index 000000000..0a9ad5cd6
--- /dev/null
+++ b/groovy-sandbox/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+
+
+ org.jenkins-ci.plugins
+ script-security-parent
+ ${changelist}
+
+
+ org.kohsuke
+ groovy-sandbox
+ https://github.com/jenkinsci/script-security-plugin
+
+ Groovy Sandbox
+ Executes untrusted Groovy script safely
+
+
+
+ false
+
+
+
+
+ org.codehaus.groovy
+ groovy
+ 2.4.21
+
+
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/GroovyInterceptor.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/GroovyInterceptor.java
new file mode 100644
index 000000000..092e7d9b9
--- /dev/null
+++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/GroovyInterceptor.java
@@ -0,0 +1,224 @@
+package org.kohsuke.groovy.sandbox;
+
+import org.kohsuke.groovy.sandbox.impl.Super;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Interceptor of Groovy method calls.
+ *
+ *
+ * Once created, it needs to be {@linkplain #register() registered} to start receiving interceptions.
+ * List of interceptors are maintained per thread.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public abstract class GroovyInterceptor {
+ /**
+ * Intercepts an instance method call on some object of the form "foo.bar(...)"
+ */
+ public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable {
+ return invoker.call(receiver,method,args);
+ }
+
+ /**
+ * Intercepts a static method call on some class, like "Class.forName(...)".
+ *
+ * Note that Groovy doesn't clearly differentiate static method calls from instance method calls.
+ * If calls are determined to be static at compile-time, you get this method called, but
+ * method calls whose receivers are {@link Class} can invoke static methods, too
+ * (that is, {@code x=Integer.class;x.valueOf(5)} results in {@code onMethodCall(invoker,Integer.class,"valueOf",5)}
+ */
+ public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable {
+ return invoker.call(receiver,method,args);
+ }
+
+ /**
+ * Intercepts an object instantiation, like "new Receiver(...)"
+ */
+ public Object onNewInstance(Invoker invoker, Class receiver, Object... args) throws Throwable {
+ return invoker.call(receiver,null,args);
+ }
+
+ /**
+ * Intercepts an super method call, like "super.foo(...)"
+ */
+ public Object onSuperCall(Invoker invoker, Class senderType, Object receiver, String method, Object... args) throws Throwable {
+ return invoker.call(new Super(senderType,receiver),method,args);
+ }
+
+ /**
+ * Intercepts a {@code super(…)} call from a constructor.
+ */
+ public void onSuperConstructor(Invoker invoker, Class receiver, Object... args) throws Throwable {
+ onNewInstance(invoker, receiver, args);
+ }
+
+ /**
+ * Intercepts a property access, like "z=foo.bar"
+ *
+ * @param receiver
+ * 'foo' in the above example, the object whose property is accessed.
+ * @param property
+ * 'bar' in the above example, the name of the property
+ */
+ public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable {
+ return invoker.call(receiver,property);
+ }
+
+ /**
+ * Intercepts a property assignment like "foo.bar=z"
+ *
+ * @param receiver
+ * 'foo' in the above example, the object whose property is accessed.
+ * @param property
+ * 'bar' in the above example, the name of the property
+ * @param value
+ * The value to be assigned.
+ * @return
+ * The result of the assignment expression. Normally, you should return the same object as {@code value}.
+ */
+ public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable {
+ return invoker.call(receiver,property,value);
+ }
+
+ /**
+ * Intercepts an attribute access, like "z=foo.@bar"
+ *
+ * @param receiver
+ * 'foo' in the above example, the object whose attribute is accessed.
+ * @param attribute
+ * 'bar' in the above example, the name of the attribute
+ */
+ public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable {
+ return invoker.call(receiver, attribute);
+ }
+
+ /**
+ * Intercepts an attribute assignment like "foo.@bar=z"
+ *
+ * @param receiver
+ * 'foo' in the above example, the object whose attribute is accessed.
+ * @param attribute
+ * 'bar' in the above example, the name of the attribute
+ * @param value
+ * The value to be assigned.
+ * @return
+ * The result of the assignment expression. Normally, you should return the same object as {@code value}.
+ */
+ public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable {
+ return invoker.call(receiver,attribute,value);
+ }
+
+ /**
+ * Intercepts an array access, like "z=foo[bar]"
+ *
+ * @param receiver
+ * 'foo' in the above example, the array-like object.
+ * @param index
+ * 'bar' in the above example, the object that acts as an index.
+ */
+ public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable {
+ return invoker.call(receiver,null,index);
+ }
+
+ /**
+ * Intercepts an attribute assignment like "foo[bar]=z"
+ *
+ * @param receiver
+ * 'foo' in the above example, the array-like object.
+ * @param index
+ * 'bar' in the above example, the object that acts as an index.
+ * @param value
+ * The value to be assigned.
+ * @return
+ * The result of the assignment expression. Normally, you should return the same object as {@code value}.
+ */
+ public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable {
+ return invoker.call(receiver,null,index,value);
+ }
+
+ /**
+ * Represents the next interceptor in the chain.
+ *
+ * As {@link GroovyInterceptor}, you intercept by doing one of the following:
+ *
+ *
+ *
Pass on to the next interceptor by calling one of the call() method,
+ * possibly modifying the arguments and return values, intercepting an exception, etc.
+ *
Throws an exception to block the call.
+ *
Return some value without calling the next interceptor.
+ *
+ *
+ * The signature of the call method is as follows:
+ *
+ *
+ *
receiver
+ *
+ * The object whose method/property is accessed.
+ * For constructor invocations and static calls, this is {@link Class}.
+ * If the receiver is null, all the interceptors will be skipped.
+ *
+ *
method
+ *
+ * The name of the method/property/attribute. Otherwise pass in null.
+ *
+ *
args
+ *
+ * Arguments of the method call, index of the array access, and/or values to be set.
+ * Multiple override of the call method is provided to avoid the implicit object
+ * array creation, but otherwise they behave the same way.
+ *
+ *
+ */
+ public interface Invoker {
+ Object call(Object receiver, String method) throws Throwable;
+ Object call(Object receiver, String method, Object arg1) throws Throwable;
+ Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable;
+ Object call(Object receiver, String method, Object... args) throws Throwable;
+ }
+
+// public void addToGlobal() {
+// globalInterceptors.add(this);
+// }
+//
+// public void removeFromGlobal() {
+// globalInterceptors.remove(this);
+// }
+
+ /**
+ * Registers this interceptor to the current thread's interceptor list.
+ */
+ public void register() {
+ threadInterceptors.get().add(this);
+ }
+
+ /**
+ * Reverses the earlier effect of {@link #register()}
+ */
+ public void unregister() {
+ threadInterceptors.get().remove(this);
+ }
+
+ private static final ThreadLocal> threadInterceptors = new ThreadLocal>() {
+ @Override
+ protected List initialValue() {
+ return new CopyOnWriteArrayList();
+ }
+ };
+
+ private static final ThreadLocal> threadInterceptorsView = new ThreadLocal>() {
+ @Override
+ protected List initialValue() {
+ return Collections.unmodifiableList(threadInterceptors.get());
+ }
+ };
+
+// private static final List globalInterceptors = new CopyOnWriteArrayList();
+
+ public static List getApplicableInterceptors() {
+ return threadInterceptorsView.get();
+ }
+}
diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/GroovyValueFilter.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/GroovyValueFilter.java
new file mode 100644
index 000000000..c75c9f86b
--- /dev/null
+++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/GroovyValueFilter.java
@@ -0,0 +1,100 @@
+package org.kohsuke.groovy.sandbox;
+
+import groovy.lang.Binding;
+import groovy.lang.Script;
+
+/**
+ * @deprecated
+ */
+@Deprecated
+public class GroovyValueFilter extends GroovyInterceptor {
+ /**
+ * Called for every receiver.
+ */
+ public Object filterReceiver(Object receiver) {
+ return filter(receiver);
+ }
+
+ /**
+ * Called for a return value of a method call, newly created object, retrieve property/attribute values.
+ */
+ public Object filterReturnValue(Object returnValue) {
+ return filter(returnValue);
+ }
+
+ /**
+ * Called for every argument to method/constructor calls.
+ */
+ public Object filterArgument(Object arg) {
+ return filter(arg);
+ }
+
+ /**
+ * Called for every index of the array get/set access.
+ */
+ public Object filterIndex(Object index) {
+ return filter(index);
+ }
+
+ /**
+ * All the specific {@code filterXXX()} methods delegate to this method.
+ */
+ public Object filter(Object o) {
+ return o;
+ }
+
+ private Object[] filterArguments(Object[] args) {
+ for (int i=0; i
+ * Sometimes you'd like to run Groovy scripts in a sandbox environment, where you only want it to
+ * access limited subset of the rest of JVM. This transformation makes that possible by letting you inspect
+ * every step of the script execution when it makes method calls and property/field/array access.
+ *
+ *
+ * Once the script is transformed, every intercepted operation results in a call to {@link Checker},
+ * which further forwards the call to {@link GroovyInterceptor} for inspection.
+ *
+ *
+ *
+ * To use it, add it to the {@link org.codehaus.groovy.control.CompilerConfiguration}, like this:
+ *
+ *
+ * def cc = new CompilerConfiguration()
+ * cc.addCompilationCustomizers(new SandboxTransformer())
+ * sh = new GroovyShell(cc)
+ *
+ *
+ *
+ * By default, this code intercepts everything that can be intercepted, which are:
+ *
+ *
Method calls (instance method and static method)
+ *
Object allocation (that is, a constructor call except of the form "this(...)" and "super(...)")
+ *
Attribute access (e.g., z=foo.@bar) and assignments (e.g., foo.@bar=z)
+ *
Array access and assignment (z=x[y] and x[y]=z)
+ *
+ *
+ * You can disable interceptions selectively by setting respective {@code interceptXXX} flags to {@code false}.
+ *
+ *
+ * There'll be a substantial hit to the performance of the execution.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class SandboxTransformer extends CompilationCustomizer {
+ /**
+ * Intercept method calls
+ */
+ boolean interceptMethodCall=true;
+ /**
+ * Intercept object instantiation by intercepting its constructor call.
+ *
+ * Note that Java byte code doesn't allow the interception of super(...) and this(...)
+ * so the object instantiation by defining and instantiating a subtype cannot be intercepted.
+ */
+ boolean interceptConstructor=true;
+ /**
+ * Intercept property access for both read "(...).y" and write "(...).y=..."
+ */
+ boolean interceptProperty=true;
+ /**
+ * Intercept array access for both read "y=a[x]" and write "a[x]=y"
+ */
+ boolean interceptArray=true;
+ /**
+ * Intercept attribute access for both read "z=x.@y" and write "x.@y=z"
+ */
+ boolean interceptAttribute=true;
+
+ public SandboxTransformer() {
+ super(CompilePhase.CANONICALIZATION);
+ }
+
+ @Override
+ public void call(final SourceUnit source, GeneratorContext context, ClassNode classNode) {
+ if (classNode == null) { // TODO is this even possible? CpsTransformer implies it is not.
+ return;
+ }
+
+ // Removes all initial expressions for constructors and methods and generates overloads for all variants.
+ new InitialExpressionExpander().expandInitialExpressions(source, classNode);
+
+ ClassCodeExpressionTransformer visitor = createVisitor(source, classNode);
+
+ processConstructors(visitor, classNode);
+ for (MethodNode m : classNode.getMethods()) {
+ forbidIfFinalizer(m);
+ visitor.visitMethod(m);
+ }
+ for (Statement s : classNode.getObjectInitializerStatements()) {
+ s.visit(visitor);
+ }
+ for (FieldNode f : classNode.getFields()) {
+ visitor.visitField(f);
+ }
+ }
+
+ /**
+ * {@link Object#finalize} is called by the JVM outside of the sandbox, so overriding it in a
+ * sandboxed script is not allowed.
+ */
+ public void forbidIfFinalizer(MethodNode m) {
+ if (m.getName().equals("finalize") && m.isVoidMethod() && !m.isPrivate() && !m.isStatic()) {
+ boolean safe = false;
+ /*
+ Groovy allows method definitions to specify default arguments for parameters. Parameters with default
+ arguments may be omitted when calling the method. Groovy implements this by generating additional
+ overloaded methods in bytecode for each variation of the method being omitted.
+ For example, given the following method:
+
+ public void finalize(int x = 0) { }
+
+ Groovy will generate 2 methods in bytecode:
+
+ public void finalize(int x) { }
+ public void finalize() { finalize(0) }
+
+ Our AST transformer will not see the generated no-arg method which overrides Object#finalize, so we need
+ to account for it by ensuring that at least one parameter does not have a default argument (AKA initial
+ expression) for the method to be acceptable, because parameters without default arguments will exist in all
+ generated methods.
+ */
+ for (Parameter p : m.getParameters()) {
+ if (!p.hasInitialExpression()) {
+ safe = true;
+ break;
+ }
+ }
+ if (!safe) {
+ throw new SecurityException("Sandboxed code may not override Object.finalize()");
+ }
+ }
+ }
+
+ /**
+ * Do not care about {@code super} calls for classes extending these types.
+ *
+ *
Entries in this list must not have any constructors with parameters whose types are not safe to construct in
+ * the sandbox, and they must be in a package that cannot be used to define new classes in the sandbox.
+ */
+ private static final Set TRIVIAL_CONSTRUCTORS = Collections.singleton(Object.class.getName());
+
+ /**
+ * Apply SECURITY-582 (and part of SECURITY-1754) fix to constructors.
+ *
+ * For example, given code like this:
+ *
{@code
+ * class B { }
+ * class A extends B {
+ * A(T1 p1, ..., TM pM) {
+ * super(U1 a1, ..., UN aN) // or `this(...)`
+ * ...
+ * }
+ * }
+ * }
+ *
+ * {@link #processConstructors} will transform it into something like this:
+ *
+ *
{@code
+ * class B { }
+ * class A extends B {
+ * A(T1 p1, ..., TM pM) {
+ * this(Checker.checkedSuperConstructor( // or `Checker.checkedThisConstructor`
+ * B.class,
+ * new Object[]{a1, ..., aN},
+ * new Object[]{p1, ..., pM},
+ * new Class>[]{SuperConstructorWrapper.class, T1.class, ..., TM.class}), // or `ThisConstructorWrapper.class`
+ * p1, ..., pM)
+ * }
+ * A(Checker.SuperConstructorWrapper $cw, T1 p1, ..., TM pM) { // Or `Checker.ThisConstructorWrapper $cw`
+ * super($cw.arg(1), ... cw.arg(N)) // or `this(...)`
+ * ...
+ * }
+ * }
+ * }
+ */
+ public void processConstructors(final ClassCodeExpressionTransformer visitor, ClassNode classNode) {
+ ClassNode superClass = classNode.getSuperClass();
+ List declaredConstructors = classNode.getDeclaredConstructors();
+ if (declaredConstructors.isEmpty()) {
+ if (classNode.isInterface()) {
+ // Interfaces are expected to not have constructors.
+ return;
+ }
+ // Default constructor should have already been added by InitialExpressionExpander
+ throw new AssertionError("No constructors for " + classNode);
+ } else {
+ declaredConstructors = new ArrayList<>(declaredConstructors);
+ }
+ for (ConstructorNode c : declaredConstructors) {
+ for (Parameter p : c.getParameters()) {
+ if (p.hasInitialExpression()) {
+ // All initial expressions should have already been removed by InitialExpressionExpander
+ throw new AssertionError("Found unexpected initial expression: " + p.getInitialExpression());
+ }
+ }
+ Statement code = c.getCode();
+ List body;
+ if (code instanceof BlockStatement) {
+ body = ((BlockStatement) code).getStatements();
+ } else {
+ body = Collections.singletonList(code);
+ }
+ ClassNode constructorCallType = ClassNode.SUPER;
+ TupleExpression constructorCallArgs = new TupleExpression();
+ if (!body.isEmpty() && body.get(0) instanceof ExpressionStatement && ((ExpressionStatement) body.get(0)).getExpression() instanceof ConstructorCallExpression) {
+ ConstructorCallExpression cce = (ConstructorCallExpression) ((ExpressionStatement) body.get(0)).getExpression();
+ if (cce.isThisCall()) {
+ constructorCallType = ClassNode.THIS;
+ body = body.subList(1, body.size());
+ constructorCallArgs = ((TupleExpression) cce.getArguments());
+ } else if (cce.isSuperCall()) {
+ body = body.subList(1, body.size());
+ constructorCallArgs = ((TupleExpression) cce.getArguments());
+ } else {
+ // Some other class, for example if `new String();` happens to be the first statement
+ // in a constructor. We handle this the same as an explicit call to `super()`.
+ }
+ }
+ // SECURITY-3341
+ // Only sandbox-transform super() constructor calls if the parent class is nontrivial. Always sandbox-transform this() constructor calls.
+ if (constructorCallType == ClassNode.SUPER && TRIVIAL_CONSTRUCTORS.contains(superClass.getName())) {
+ visitor.visitMethod(c);
+ continue;
+ }
+ final TupleExpression _constructorCallArgs = constructorCallArgs;
+ final AtomicReference constructorCallArgsTransformed = new AtomicReference<>();
+ ((ScopeTrackingClassCodeExpressionTransformer) visitor).withMethod(c, new Runnable() {
+ @Override
+ public void run() {
+ constructorCallArgsTransformed.set(((VisitorImpl) visitor).transformArguments(_constructorCallArgs));
+ }
+ });
+ // Create parameters for new constructor.
+ Parameter[] origParams = c.getParameters();
+ Parameter[] params = new Parameter[origParams.length + 1];
+ params[0] = new Parameter(new ClassNode(constructorCallType == ClassNode.THIS ? Checker.ThisConstructorWrapper.class : Checker.SuperConstructorWrapper.class), "$cw");
+ System.arraycopy(origParams, 0, params, 1, origParams.length);
+ List paramTypes = new ArrayList<>(params.length);
+ for (Parameter p : params) {
+ paramTypes.add(new ClassExpression(p.getType()));
+ }
+ // Create arguments for call to synthetic constructor.
+ List thisArgs = new ArrayList<>(origParams.length + 1);
+ thisArgs.add(null); // Placeholder
+ List thisArgsWithoutWrapper = new ArrayList<>(origParams.length);
+ for (Parameter p : origParams) {
+ if (p.getType().equals(superConstructorWrapperClass) || p.getType().equals(thisConstructorWrapperClass)) {
+ throw new SecurityException("Illegal constructor parameter for " + classNode + ": " + p);
+ }
+ thisArgs.add(new VariableExpression(p));
+ thisArgsWithoutWrapper.add(new VariableExpression(p));
+ }
+ if (constructorCallType == ClassNode.THIS) {
+ thisArgs.set(0, ((VisitorImpl) visitor).makeCheckedCall("checkedThisConstructor",
+ new ClassExpression(classNode),
+ constructorCallArgsTransformed.get(),
+ new ArrayExpression(new ClassNode(Object.class), thisArgsWithoutWrapper),
+ new ArrayExpression(new ClassNode(Class.class), paramTypes)));
+ } else {
+ thisArgs.set(0, ((VisitorImpl) visitor).makeCheckedCall("checkedSuperConstructor",
+ new ClassExpression(classNode),
+ new ClassExpression(superClass),
+ constructorCallArgsTransformed.get(),
+ new ArrayExpression(new ClassNode(Object.class), thisArgsWithoutWrapper),
+ new ArrayExpression(new ClassNode(Class.class), paramTypes)));
+ }
+ c.setCode(new BlockStatement(new Statement[] {new ExpressionStatement(new ConstructorCallExpression(ClassNode.THIS, new TupleExpression(thisArgs)))}, c.getVariableScope()));
+ List cwArgs = new ArrayList<>();
+ int x = 0;
+ for (Expression constructorCallArg : constructorCallArgs) {
+ cwArgs.add(/*new CastExpression(superArg.getType(), */new MethodCallExpression(new VariableExpression("$cw"), "arg", new ConstantExpression(x++))/*)*/);
+ }
+ List body2 = new ArrayList<>(body.size() + 1);
+ body2.add(0, new ExpressionStatement(new ConstructorCallExpression(constructorCallType, new ArgumentListExpression(cwArgs))));
+ body2.addAll(body);
+ ((ScopeTrackingClassCodeExpressionTransformer) visitor).withMethod(c, () -> {
+ for (int i = 1; i < body2.size(); i++) { // Skip the first statement, which is the constructor call.
+ body2.get(i).visit(visitor);
+ }
+ });
+ final int SYNTHETIC = 0x00001000; // Not public in Modifier
+ ConstructorNode c2 = new ConstructorNode(Modifier.PRIVATE | SYNTHETIC, params, c.getExceptions(), new BlockStatement(body2, c.getVariableScope()));
+ // perhaps more misleading than helpful: c2.setSourcePosition(c);
+ classNode.addConstructor(c2);
+ }
+ }
+
+ @Deprecated
+ public ClassCodeExpressionTransformer createVisitor(SourceUnit source) {
+ return createVisitor(source, null);
+ }
+
+ public ClassCodeExpressionTransformer createVisitor(SourceUnit source, ClassNode clazz) {
+ return new VisitorImpl(source, clazz);
+ }
+
+ class VisitorImpl extends ScopeTrackingClassCodeExpressionTransformer {
+ private final SourceUnit sourceUnit;
+ /**
+ * Invocation/property access without the left-hand side expression (for example {@code foo()}
+ * as opposed to {@code something.foo()} means {@code this.foo()} in Java, but this is not
+ * so in Groovy.
+ *
+ * In Groovy, {@code foo()} inside a closure uses the closure object itself as the lhs value,
+ * whereas {@code this} in closure refers to a nearest enclosing non-closure object.
+ *
+ * So we cannot always expand {@code foo()} to {@code this.foo()}.
+ *
+ * To keep track of when we can expand {@code foo()} to {@code this.foo()} and when we can't,
+ * we maintain this flag as we visit the expression tree. This flag is set to true
+ * while we are visiting the body of the closure (the part between { ... }), and switched
+ * back to false as we visit inner classes.
+ *
+ * To correctly expand {@code foo()} in the closure requires an access to the closure object itself,
+ * and unfortunately Groovy doesn't seem to have any reliable way to do this. The hack I came up
+ * with is {@code asWritable().getOwner()}, but even that is subject to the method resolution rule.
+ *
+ */
+ private boolean visitingClosureBody;
+
+ /**
+ * Current class we are traversing.
+ */
+ private ClassNode clazz;
+
+ /**
+ * Return type of the current method or closure body that we are traversing.
+ */
+ private ClassNode methodReturnType;
+
+ VisitorImpl(SourceUnit sourceUnit, ClassNode clazz) {
+ this.sourceUnit = sourceUnit;
+ this.clazz = clazz;
+ }
+
+ @Override
+ public void visitMethod(MethodNode node) {
+ if (clazz == null) { // compatibility
+ clazz = node.getDeclaringClass();
+ }
+ methodReturnType = node.getReturnType();
+ try {
+ // Add explicit return statements so we can insert casts as needed.
+ ReturnAdder adder = new ReturnAdder();
+ adder.visitMethod(node);
+ super.visitMethod(node);
+ } finally {
+ methodReturnType = null;
+ }
+ }
+
+ @Override
+ public void visitReturnStatement(ReturnStatement statement) {
+ if (statement.isReturningNullOrVoid()) {
+ // We must not mutate ReturnStatement.RETURN_NULL_OR_VOID, and we don't care about casting null anyway.
+ return;
+ }
+ super.visitReturnStatement(statement);
+ // statement.getExpression has already been transformed by the super call, so we do not transform it twice.
+ statement.setExpression(makeCheckedGroovyCast(methodReturnType, statement.getExpression()));
+ }
+
+ @Override
+ public void visitField(FieldNode node) {
+ super.visitField(node);
+ // When using @Field with a declaration that has no default value, node.getInitialExpression is an
+ // EmptyExpression rather than null, so we must ignore it to avoid breaking things.
+ if (node.hasInitialExpression() && !(node.getInitialValueExpression() instanceof EmptyExpression)) {
+ // node.getInitialValueExpression has already been transformed by the super call, so we do not transform it twice.
+ node.setInitialValueExpression(makeCheckedGroovyCast(node.getType(), node.getInitialValueExpression()));
+ }
+ }
+
+ /**
+ * Transforms the arguments of a call.
+ * Groovy primarily uses {@link ArgumentListExpression} for this,
+ * but the signature doesn't guarantee that. So this method takes care of that.
+ */
+ Expression transformArguments(Expression e) {
+ List l;
+ if (e instanceof TupleExpression) {
+ List expressions = ((TupleExpression) e).getExpressions();
+ l = new ArrayList<>(expressions.size());
+ for (Expression expression : expressions) {
+ l.add(transform(expression));
+ }
+ } else {
+ l = Collections.singletonList(transform(e));
+ }
+
+ // checkdCall expects an array
+ return withLoc(e,new MethodCallExpression(new ListExpression(l),"toArray",new ArgumentListExpression()));
+ }
+
+ Expression makeCheckedCall(String name, Expression... arguments) {
+ return new StaticMethodCallExpression(checkerClass,name,
+ new ArgumentListExpression(arguments));
+ }
+
+ /**
+ * Groovy implicitly casts some expressions at runtime, so we manually insert explicit casts as needed to
+ * intercept potentially dangerous calls.
+ */
+ Expression makeCheckedGroovyCast(ClassNode clazz, Expression value) {
+ if (isKnownSafeCast(clazz, value)) {
+ return value;
+ }
+ return makeCheckedCall("checkedCast",
+ classExp(clazz),
+ value,
+ boolExp(false),
+ boolExp(false), // Groovy evaluates implicit casts using ScriptByteCodeAdapter.castToType, so coerce must be false.
+ boolExp(false));
+ }
+
+ @Override
+ public Expression transform(Expression exp) {
+ Expression o = innerTransform(exp);
+ if (o!=exp) {
+ o.setSourcePosition(exp);
+ }
+ return o;
+ }
+
+ private Expression innerTransform(Expression exp) {
+ if (exp instanceof ClosureExpression) {
+ // ClosureExpression.transformExpression doesn't visit the code inside
+ ClosureExpression ce = (ClosureExpression)exp;
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ Parameter[] parameters = ce.getParameters();
+ if (parameters != null) {
+ // Explicitly defined parameters, i.e., ".findAll { i -> i == 'bar' }"
+ if (parameters.length > 0) {
+ for (Parameter p : parameters) {
+ if (p.hasInitialExpression()) {
+ Expression init = p.getInitialExpression();
+ p.setInitialExpression(makeCheckedGroovyCast(p.getType(), transform(init)));
+ }
+ }
+ for (Parameter p : parameters) {
+ declareVariable(p);
+ }
+ } else {
+ // Implicit parameter - i.e., ".findAll { it == 'bar' }"
+ declareVariable(new Parameter(ClassHelper.DYNAMIC_TYPE, "it"));
+ }
+ }
+ boolean old = visitingClosureBody;
+ visitingClosureBody = true;
+ ClassNode oldMethodReturnType = methodReturnType;
+ methodReturnType = ClassHelper.OBJECT_TYPE;
+ try {
+ ce.getCode().visit(this);
+ } finally {
+ visitingClosureBody = old;
+ methodReturnType = oldMethodReturnType;
+ }
+ }
+ }
+
+ if (exp instanceof MethodCallExpression && interceptMethodCall) {
+ // lhs.foo(arg1,arg2) => checkedCall(lhs,"foo",arg1,arg2)
+ // lhs+rhs => lhs.plus(rhs)
+ // Integer.plus(Integer) => DefaultGroovyMethods.plus
+ // lhs || rhs => lhs.or(rhs)
+ MethodCallExpression call = (MethodCallExpression) exp;
+
+ Expression objExp;
+ if (call.isImplicitThis() && visitingClosureBody && !isLocalVariableExpression(call.getObjectExpression()))
+ objExp = CLOSURE_THIS;
+ else
+ objExp = transform(call.getObjectExpression());
+
+ Expression arg1 = transform(call.getMethod());
+ Expression arg2 = transformArguments(call.getArguments());
+
+ if (call.getObjectExpression() instanceof VariableExpression && ((VariableExpression) call.getObjectExpression()).getName().equals("super")) {
+ if (clazz == null) {
+ throw new IllegalStateException("owning class not defined");
+ }
+ return makeCheckedCall("checkedSuperCall", new ClassExpression(clazz), objExp, arg1, arg2);
+ } else {
+ return makeCheckedCall("checkedCall",
+ objExp,
+ boolExp(call.isSafe()),
+ boolExp(call.isSpreadSafe()),
+ arg1,
+ arg2);
+ }
+ }
+
+ if (exp instanceof StaticMethodCallExpression && interceptMethodCall) {
+ /*
+ Groovy doesn't use StaticMethodCallExpression as much as it could in compilation.
+ For example, "Math.max(1,2)" results in a regular MethodCallExpression.
+
+ Static import handling uses StaticMethodCallExpression, and so are some
+ ASTTransformations like ToString,EqualsAndHashCode, etc.
+ */
+ StaticMethodCallExpression call = (StaticMethodCallExpression) exp;
+ return makeCheckedCall("checkedStaticCall",
+ new ClassExpression(call.getOwnerType()),
+ new ConstantExpression(call.getMethod()),
+ transformArguments(call.getArguments())
+ );
+ }
+
+ if (exp instanceof MethodPointerExpression && interceptMethodCall) {
+ MethodPointerExpression mpe = (MethodPointerExpression) exp;
+ return new ConstructorCallExpression(
+ new ClassNode(SandboxedMethodClosure.class),
+ new ArgumentListExpression(
+ transform(mpe.getExpression()),
+ transform(mpe.getMethodName()))
+ );
+ }
+
+ if (exp instanceof ConstructorCallExpression && interceptConstructor) {
+ if (!((ConstructorCallExpression) exp).isSpecialCall()) {
+ // creating a new instance, like "new Foo(...)"
+ return makeCheckedCall("checkedConstructor",
+ new ClassExpression(exp.getType()),
+ transformArguments(((ConstructorCallExpression) exp).getArguments())
+ );
+ } else {
+ // we can't really intercept constructor calling super(...) or this(...),
+ // since it has to be the first method call in a constructor.
+ // but see SECURITY-582 fix above
+ }
+ }
+
+ if (exp instanceof AttributeExpression && interceptAttribute) {
+ AttributeExpression ae = (AttributeExpression) exp;
+ return makeCheckedCall("checkedGetAttribute",
+ transform(ae.getObjectExpression()),
+ boolExp(ae.isSafe()),
+ boolExp(ae.isSpreadSafe()),
+ transform(ae.getProperty())
+ );
+ }
+
+ if (exp instanceof PropertyExpression && interceptProperty) {
+ PropertyExpression pe = (PropertyExpression) exp;
+ return makeCheckedCall("checkedGetProperty",
+ transformObjectExpression(pe),
+ boolExp(pe.isSafe()),
+ boolExp(pe.isSpreadSafe()),
+ transform(pe.getProperty())
+ );
+ }
+
+ if (exp instanceof FieldExpression && interceptProperty) {
+ // I am not sure whether this is reachable. See note below regarding the only known case of FieldExpression in the AST.
+ FieldExpression fe = (FieldExpression) exp;
+ return makeCheckedCall("checkedGetAttribute",
+ new VariableExpression("this"),
+ boolExp(false),
+ boolExp(false),
+ stringExp(fe.getFieldName())
+ );
+ }
+
+ if (exp instanceof VariableExpression && interceptProperty) {
+ VariableExpression vexp = (VariableExpression) exp;
+ if (isLocalVariable(vexp.getName()) || vexp.getName().equals("this") || vexp.getName().equals("super")) {
+ // We don't care what sandboxed code does to itself until it starts interacting with outside world
+ return super.transform(exp);
+ } else {
+ // if the variable is not in-scope local variable, it gets treated as a property access with implicit this.
+ // see AsmClassGenerator.visitVariableExpression and processClassVariable.
+ PropertyExpression pexp = new PropertyExpression(VariableExpression.THIS_EXPRESSION, vexp.getName());
+ pexp.setImplicitThis(true);
+ withLoc(exp,pexp);
+ return transform(pexp);
+ }
+ }
+
+ if (exp instanceof DeclarationExpression) {
+ handleDeclarations((DeclarationExpression) exp);
+ // We handle DeclarationExpression here to simplify handling of BinaryExpression for non-declaration assignments.
+ DeclarationExpression de = (DeclarationExpression) exp;
+ Expression rhs = de.getRightExpression();
+ if (rhs instanceof EmptyExpression) {
+ // Declaration without initialization.
+ return exp;
+ } else if (de.isMultipleAssignmentDeclaration()) {
+ throw new UnsupportedOperationException("The sandbox does not currently support multiple assignment");
+ }
+ return withLoc(de, new DeclarationExpression(de.getVariableExpression(), de.getOperation(),
+ makeCheckedGroovyCast(de.getVariableExpression().getType(), transform(rhs))));
+ }
+
+ if (exp instanceof BinaryExpression) {
+ BinaryExpression be = (BinaryExpression) exp;
+ // this covers everything from a+b to a=b
+ if (ofType(be.getOperation().getType(),ASSIGNMENT_OPERATOR)) {
+ // simple assignment like '=' as well as compound assignments like "+=","-=", etc.
+
+ // How we dispatch this depends on the type of left expression.
+ //
+ // What can be LHS?
+ // according to AsmClassGenerator, PropertyExpression, AttributeExpression, FieldExpression, VariableExpression
+ // Can also be TupleExpression, but we do not currently handle that.
+
+ Expression lhs = be.getLeftExpression();
+ if (lhs instanceof VariableExpression) {
+ VariableExpression vexp = (VariableExpression) lhs;
+ if (isLocalVariable(vexp.getName()) || vexp.getName().equals("this") || vexp.getName().equals("super")) {
+ return withLoc(be, new BinaryExpression(lhs, be.getOperation(),
+ makeCheckedGroovyCast(vexp.getType(), transform(be.getRightExpression()))));
+ } else {
+ // if the variable is not in-scope local variable, it gets treated as a property access with implicit this.
+ // see AsmClassGenerator.visitVariableExpression and processClassVariable.
+ PropertyExpression pexp = new PropertyExpression(VariableExpression.THIS_EXPRESSION, vexp.getName());
+ pexp.setImplicitThis(true);
+ pexp.setSourcePosition(vexp);
+
+ lhs = pexp;
+ }
+ } // no else here
+ if (lhs instanceof PropertyExpression) {
+ PropertyExpression pe = (PropertyExpression) lhs;
+ String name = null;
+ if (lhs instanceof AttributeExpression) {
+ if (interceptAttribute)
+ name = "checkedSetAttribute";
+ } else {
+ Expression receiver = pe.getObjectExpression();
+ if (receiver instanceof VariableExpression && ((VariableExpression) receiver).isThisExpression()) {
+ FieldNode field = clazz != null ? clazz.getField(pe.getPropertyAsString()) : null;
+ if (field != null) {
+ // "this.x = y" must be handled specially to prevent the sandbox from using
+ // reflection to assign values to final fields in constructors and initializers
+ // and to prevent infinite loops in setter methods.
+ Token op = be.getOperation();
+ if (op.getType() == Types.ASSIGN) {
+ return new BinaryExpression(new FieldExpression(field), op,
+ makeCheckedGroovyCast(field.getType(), transform(be.getRightExpression())));
+ } else {
+ // Groovy does not support FieldExpression with compound assignment operators
+ // directly, so we must expand the expression ourselves.
+ Token plainAssignment = Token.newSymbol(Types.ASSIGN, op.getStartLine(), op.getStartColumn());
+ return new BinaryExpression(new FieldExpression(field), plainAssignment,
+ makeCheckedGroovyCast(field.getType(),
+ makeCheckedCall("checkedBinaryOp",
+ new FieldExpression(field),
+ intExp(Ops.compoundAssignmentToBinaryOperator(op.getType())),
+ transform(be.getRightExpression()))));
+ }
+ } // else this is a property which we need to check
+ }
+ if (interceptProperty)
+ name = "checkedSetProperty";
+ }
+ if (name==null) // not intercepting?
+ return super.transform(exp);
+
+ return makeCheckedCall(name,
+ transformObjectExpression(pe),
+ transform(pe.getProperty()),
+ boolExp(pe.isSafe()),
+ boolExp(pe.isSpreadSafe()),
+ intExp(be.getOperation().getType()),
+ transform(be.getRightExpression())
+ );
+ } else
+ if (lhs instanceof FieldExpression) {
+ // The only known occurrences of this expression in the AST are for the `this$0` field that is
+ // added to anonymous and inner classes to allow them to access their outer class and for
+ // assigning the values of static enum constant fields in synthetically generated enum constructors.
+ FieldExpression fe = (FieldExpression) lhs;
+ if (fe.getField().isFinal()) {
+ // Assignments to final fields cannot be done reflectively, so we leave FieldExpression untransformed.
+ return withLoc(be, new BinaryExpression(fe, be.getOperation(),
+ makeCheckedGroovyCast(fe.getType(), transform(be.getRightExpression()))));
+ }
+ return withLoc(be, makeCheckedCall("checkedSetAttribute",
+ new VariableExpression("this"),
+ stringExp(fe.getFieldName()),
+ boolExp(false),
+ boolExp(false),
+ intExp(be.getOperation().getType()),
+ makeCheckedGroovyCast(fe.getType(), transform(be.getRightExpression()))));
+ } else
+ if (lhs instanceof BinaryExpression) {
+ BinaryExpression lbe = (BinaryExpression) lhs;
+ if (lbe.getOperation().getType()==Types.LEFT_SQUARE_BRACKET && interceptArray) {// expression of the form "x[y] = z"
+ return makeCheckedCall("checkedSetArray",
+ transform(lbe.getLeftExpression()),
+ transform(lbe.getRightExpression()),
+ intExp(be.getOperation().getType()),
+ transform(be.getRightExpression())
+ );
+ }
+ } else if (lhs instanceof TupleExpression) {
+ throw new UnsupportedOperationException("The sandbox does not support multiple assignment");
+ }
+ throw new AssertionError("Unexpected LHS of an assignment: " + lhs.getClass());
+ }
+ if (be.getOperation().getType()==Types.LEFT_SQUARE_BRACKET) {// array reference
+ if (interceptArray)
+ return makeCheckedCall("checkedGetArray",
+ transform(be.getLeftExpression()),
+ transform(be.getRightExpression())
+ );
+ } else
+ if (be.getOperation().getType()==Types.KEYWORD_INSTANCEOF) {// instanceof operator
+ return super.transform(exp);
+ } else
+ if (Ops.isLogicalOperator(be.getOperation().getType())) {
+ return super.transform(exp);
+ } else
+ if (be.getOperation().getType()==Types.KEYWORD_IN) {// membership operator: JENKINS-28154
+ // This requires inverted operand order:
+ // "a in [...]" -> "[...].isCase(a)"
+ if (interceptMethodCall)
+ return makeCheckedCall("checkedCall",
+ transform(be.getRightExpression()),
+ boolExp(false),
+ boolExp(false),
+ stringExp("isCase"),
+ transform(be.getLeftExpression())
+
+ );
+ } else
+ if (Ops.isRegexpComparisonOperator(be.getOperation().getType())) {
+ if (interceptMethodCall)
+ return makeCheckedCall("checkedStaticCall",
+ classExp(ScriptBytecodeAdapterClass),
+ stringExp(Ops.binaryOperatorMethods(be.getOperation().getType())),
+ transform(be.getLeftExpression()),
+ transform(be.getRightExpression())
+ );
+ } else
+ if (Ops.isComparisionOperator(be.getOperation().getType())) {
+ if (interceptMethodCall) {
+ return makeCheckedCall("checkedComparison",
+ transform(be.getLeftExpression()),
+ intExp(be.getOperation().getType()),
+ transform(be.getRightExpression())
+ );
+ }
+ } else
+ if (interceptMethodCall) {
+ // normally binary operators like a+b
+ // TODO: check what other weird binary operators land here
+ return makeCheckedCall("checkedBinaryOp",
+ transform(be.getLeftExpression()),
+ intExp(be.getOperation().getType()),
+ transform(be.getRightExpression())
+ );
+ }
+ }
+
+ if (exp instanceof PostfixExpression) {
+ PostfixExpression pe = (PostfixExpression) exp;
+ return prefixPostfixExp(exp, pe.getExpression(), pe.getOperation(), "Postfix");
+ }
+ if (exp instanceof PrefixExpression) {
+ PrefixExpression pe = (PrefixExpression) exp;
+ return prefixPostfixExp(exp, pe.getExpression(), pe.getOperation(), "Prefix");
+ }
+
+ if (exp instanceof CastExpression) {
+ CastExpression ce = (CastExpression) exp;
+ return makeCheckedCall("checkedCast",
+ classExp(exp.getType()),
+ transform(ce.getExpression()),
+ boolExp(ce.isIgnoringAutoboxing()),
+ boolExp(ce.isCoerce()),
+ boolExp(ce.isStrict())
+ );
+ }
+
+ if (exp instanceof BitwiseNegationExpression) {
+ BitwiseNegationExpression bne = (BitwiseNegationExpression) exp;
+ return makeCheckedCall("checkedBitwiseNegate", transform(bne.getExpression()));
+ }
+
+ if (exp instanceof RangeExpression) {
+ RangeExpression re = (RangeExpression) exp;
+ return makeCheckedCall("checkedCreateRange",
+ transform(re.getFrom()),
+ transform(re.getTo()),
+ boolExp(re.isInclusive()));
+ }
+
+ if (exp instanceof UnaryMinusExpression) {
+ UnaryMinusExpression ume = (UnaryMinusExpression) exp;
+ return makeCheckedCall("checkedUnaryMinus", transform(ume.getExpression()));
+ }
+
+ if (exp instanceof UnaryPlusExpression) {
+ UnaryPlusExpression upe = (UnaryPlusExpression) exp;
+ return makeCheckedCall("checkedUnaryPlus", transform(upe.getExpression()));
+ }
+
+ return super.transform(exp);
+ }
+
+ // 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
+ private Expression prefixPostfixExp(Expression whole, Expression atom, Token opToken, String mode) {
+ String op = opToken.getText().equals("++") ? "next" : "previous";
+
+ // a[b]++
+ if (atom instanceof BinaryExpression && ((BinaryExpression) atom).getOperation().getType()==Types.LEFT_SQUARE_BRACKET && interceptArray) {
+ return makeCheckedCall("checked" + mode + "Array",
+ transform(((BinaryExpression) atom).getLeftExpression()),
+ transform(((BinaryExpression) atom).getRightExpression()),
+ stringExp(op)
+ );
+ }
+
+ // a++
+ if (atom instanceof VariableExpression) {
+ VariableExpression ve = (VariableExpression) atom;
+ if (isLocalVariable(ve.getName())) {
+ if (mode.equals("Postfix")) {
+ // a trick to rewrite a++ without introducing a new local variable
+ // a++ -> [a,a=a.next()][0]
+ return transform(withLoc(whole,new BinaryExpression(
+ new ListExpression(Arrays.asList(
+ atom,
+ new BinaryExpression(atom, ASSIGNMENT_OP,
+ withLoc(atom,new MethodCallExpression(atom,op,EMPTY_ARGUMENTS)))
+ )),
+ new Token(Types.LEFT_SQUARE_BRACKET, "[", -1,-1),
+ new ConstantExpression(0)
+ )));
+ } else {
+ // ++a -> a=a.next()
+ return transform(withLoc(whole,new BinaryExpression(atom,ASSIGNMENT_OP,
+ withLoc(atom,new MethodCallExpression(atom,op,EMPTY_ARGUMENTS)))
+ ));
+ }
+ } else {
+ // if the variable is not in-scope local variable, it gets treated as a property access with implicit this.
+ // see AsmClassGenerator.visitVariableExpression and processClassVariable.
+ PropertyExpression pexp = new PropertyExpression(VariableExpression.THIS_EXPRESSION, ve.getName());
+ pexp.setImplicitThis(true);
+ pexp.setSourcePosition(atom);
+
+ atom = pexp;
+ // fall through to the "a.b++" case below
+ }
+ }
+
+ // a.@b++
+ if (atom instanceof AttributeExpression && interceptProperty) {
+ AttributeExpression ae = (AttributeExpression) atom;
+ return makeCheckedCall("checked" + mode + "Attribute",
+ transformObjectExpression(ae),
+ transform(ae.getProperty()),
+ boolExp(ae.isSafe()),
+ boolExp(ae.isSpreadSafe()),
+ stringExp(op)
+ );
+ }
+
+ // a.b++
+ if (atom instanceof PropertyExpression && interceptProperty) {
+ PropertyExpression pe = (PropertyExpression) atom;
+ return makeCheckedCall("checked" + mode + "Property",
+ transformObjectExpression(pe),
+ transform(pe.getProperty()),
+ boolExp(pe.isSafe()),
+ boolExp(pe.isSpreadSafe()),
+ stringExp(op)
+ );
+ }
+
+ // this.b++ where this.b is a FieldExpression.
+ // It is unclear if this is actually reachable. I think that syntax like `this.b` will always be a
+ // PropertyExpression in this context. We handle it explicitly as a precaution, since the catch-all
+ // below does not store the result, which would definitely be wrong for FieldExpression.
+ if (atom instanceof FieldExpression) {
+ FieldExpression fe = (FieldExpression) atom;
+ return makeCheckedCall("checked" + mode + "Attribute",
+ new VariableExpression("this"),
+ stringExp(fe.getFieldName()),
+ boolExp(false),
+ boolExp(false),
+ stringExp(op)
+ );
+ }
+
+ // method()++, 1++, any other cases where "atom" is not valid as the LHS of an assignment expression, so no
+ // store is performed, see https://github.com/apache/groovy/blob/GROOVY_2_4_7/src/main/org/codehaus/groovy/classgen/asm/BinaryExpressionHelper.java#L724.
+ if (mode.equals("Postfix")) {
+ // We need to intercept the call to x.next() while making sure that x is not evaluated more than once.
+ // x++ -> (temp -> { temp.next(); temp }(x))
+ VariableScope closureScope = new VariableScope();
+ ClosureExpression closure = withLoc(whole, new ClosureExpression(
+ new Parameter[] { new Parameter(ClassHelper.DYNAMIC_TYPE, "temp") },
+ new BlockStatement(
+ Arrays.asList(
+ new ExpressionStatement(new MethodCallExpression(
+ new VariableExpression("temp"), op, EMPTY_ARGUMENTS)),
+ new ExpressionStatement(new VariableExpression("temp"))),
+ new VariableScope(closureScope))));
+ closure.setVariableScope(closureScope);
+ return transform(withLoc(whole,
+ new MethodCallExpression(closure, "call", new ArgumentListExpression(atom))));
+ } else {
+ // ++x -> x.next()
+ return transform(withLoc(whole, new MethodCallExpression(atom, op, EMPTY_ARGUMENTS)));
+ }
+ }
+
+ /**
+ * Decorates an {@link ASTNode} by copying source location from another node.
+ */
+ private T withLoc(ASTNode src, T t) {
+ t.setSourcePosition(src);
+ return t;
+ }
+
+ /**
+ * See {@link #visitingClosureBody} for the details of what this method is about.
+ */
+ private Expression transformObjectExpression(PropertyExpression exp) {
+ if (exp.isImplicitThis() && visitingClosureBody && !isLocalVariableExpression(exp.getObjectExpression())) {
+ return CLOSURE_THIS;
+ } else {
+ return transform(exp.getObjectExpression());
+ }
+ }
+
+ private boolean isLocalVariableExpression(Expression exp) {
+ if (exp != null && exp instanceof VariableExpression) {
+ return isLocalVariable(((VariableExpression) exp).getName());
+ }
+
+ return false;
+ }
+
+ ConstantExpression boolExp(boolean v) {
+ return v ? ConstantExpression.PRIM_TRUE : ConstantExpression.PRIM_FALSE;
+ }
+
+ ConstantExpression intExp(int v) {
+ return new ConstantExpression(v,true);
+ }
+
+ ClassExpression classExp(ClassNode c) {
+ return new ClassExpression(c);
+ }
+
+ ConstantExpression stringExp(String v) {
+ return new ConstantExpression(v);
+ }
+
+ @Override
+ protected SourceUnit getSourceUnit() {
+ return sourceUnit;
+ }
+ }
+
+ // Subclassing is required because the methods we need have protected visibility in Verifier.
+ public static class InitialExpressionExpander extends Verifier {
+ public void expandInitialExpressions(SourceUnit source, ClassNode node) {
+ super.setClassNode(node);
+ if (node.isInterface()) {
+ return;
+ }
+ super.addDefaultParameterMethods(node);
+ super.addDefaultParameterConstructors(node);
+ super.addDefaultConstructor(node);
+ // addDefaultParameterMethods introduces VariablesExpressions with a null getAccessedVariable(), so we
+ // rerun VariableScopeVisitor to prevent issues when this is used by groovy-cps.
+ new VariableScopeVisitor(source).visitClass(node);
+ }
+ }
+
+ /**
+ * Return true if this cast is statically known to be safe and does not need to be checked at runtime.
+ *
+ * @see Checker#preCheckedCast
+ */
+ public static boolean isKnownSafeCast(ClassNode type, Expression exp) {
+ if (exp.getType().isDerivedFrom(type) || exp.getType().implementsInterface(type)) {
+ return true;
+ } else if (exp instanceof ConstantExpression && ((ConstantExpression)exp).isNullExpression()) {
+ return true;
+ } else if (exp instanceof EmptyExpression) {
+ // If we get here, something has already gone wrong, and inserting a checked cast would make things even worse.
+ return true;
+ }
+ return false;
+ }
+
+ static final ClassNode checkerClass = new ClassNode(Checker.class);
+ static final ClassNode ScriptBytecodeAdapterClass = new ClassNode(ScriptBytecodeAdapter.class);
+ static final ClassNode superConstructorWrapperClass = new ClassNode(Checker.SuperConstructorWrapper.class);
+ static final ClassNode thisConstructorWrapperClass = new ClassNode(Checker.ThisConstructorWrapper.class);
+
+ /**
+ * Expression that accesses the closure object itself from within the closure.
+ *
+ * Currently a hacky "asWritable().getOwner()"
+ */
+ static final Expression CLOSURE_THIS;
+
+ static {
+ MethodCallExpression aw = new MethodCallExpression(new VariableExpression("this"),"asWritable",EMPTY_ARGUMENTS);
+ aw.setImplicitThis(true);
+
+ CLOSURE_THIS = new MethodCallExpression(aw,"getOwner",EMPTY_ARGUMENTS);
+ }
+}
diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/ScopeTrackingClassCodeExpressionTransformer.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/ScopeTrackingClassCodeExpressionTransformer.java
new file mode 100644
index 000000000..09271ebbf
--- /dev/null
+++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/ScopeTrackingClassCodeExpressionTransformer.java
@@ -0,0 +1,248 @@
+package org.kohsuke.groovy.sandbox;
+
+import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.Variable;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.BooleanExpression;
+import org.codehaus.groovy.ast.expr.CastExpression;
+import org.codehaus.groovy.ast.expr.DeclarationExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.TupleExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.CatchStatement;
+import org.codehaus.groovy.ast.stmt.DoWhileStatement;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.ForStatement;
+import org.codehaus.groovy.ast.stmt.IfStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.SwitchStatement;
+import org.codehaus.groovy.ast.stmt.SynchronizedStatement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
+import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.syntax.Token;
+import org.codehaus.groovy.syntax.Types;
+
+/**
+ * Keeps track of in-scope variables.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+abstract class ScopeTrackingClassCodeExpressionTransformer extends ClassCodeExpressionTransformer {
+ /**
+ * As we visit expressions, track variable scopes.
+ * This is used to distinguish local variables from property access. See issue #11.
+ */
+ StackVariableSet varScope;
+
+ static final Token ASSIGNMENT_OP = new Token(Types.ASSIGN, "=", -1, -1);
+
+ public boolean isLocalVariable(String name) {
+ return varScope.has(name);
+ }
+
+ @Override
+ public void visitMethod(MethodNode node) {
+ varScope = null;
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ for (Parameter p : node.getParameters()) {
+ declareVariable(p);
+ }
+ super.visitMethod(node);
+ }
+ }
+
+ void withMethod(MethodNode node, Runnable r) {
+ varScope = null;
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ for (Parameter p : node.getParameters()) {
+ declareVariable(p);
+ }
+ r.run();
+ }
+ }
+
+ @Override
+ public void visitField(FieldNode node) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ super.visitField(node);
+ }
+ }
+
+ @Override
+ public void visitBlockStatement(BlockStatement block) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ super.visitBlockStatement(block);
+ }
+ }
+
+ @Override
+ public void visitDoWhileLoop(DoWhileStatement loop) {
+ // Do-while loops are not actually supported by Groovy 2.x.
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ loop.getLoopBlock().visit(this);
+ }
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ loop.setBooleanExpression((BooleanExpression) transform(loop.getBooleanExpression()));
+ }
+ }
+
+ @Override
+ public void visitForLoop(ForStatement forLoop) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ /*
+ Groovy appears to always treat the left-hand side of forLoop as a declaration.
+ i.e., the following code is error
+
+ def h() {
+ def x =0;
+ def i = 0;
+ for (i in 0..9 ) {
+ x+= i;
+ }
+ println x;
+ }
+
+ script1414457812466.groovy: 18: The current scope already contains a variable of the name i
+ @ line 18, column 5.
+ for (i in 0..9 ) {
+ ^
+
+ 1 error
+
+ Also see issue 17.
+ */
+ if (!ForStatement.FOR_LOOP_DUMMY.equals(forLoop.getVariable())) {
+ // When using Java-style for loops, the 3 expressions are a ClosureListExpression and ForStatement.getVariable is a dummy value that we need to ignore.
+ declareVariable(forLoop.getVariable());
+ rewriteForEachImplicitCast(forLoop);
+ }
+ // Avoid super.visitForLoop because it transforms the collection expression but then recurses on the entire
+ // ForStatement, causing the collection expression to be visited a second time.
+ forLoop.setCollectionExpression(transform(forLoop.getCollectionExpression()));
+ forLoop.getLoopBlock().visit(this);
+ }
+ }
+
+ /**
+ * SECURITY-3792: intercepts the per-element implicit cast that Groovy emits for a typed for-each
+ * loop, {@code for (T v in collection)}. Groovy casts each element to {@code T} when storing it
+ * into the loop variable by emitting {@code ScriptBytecodeAdapter.castToType} during bytecode
+ * generation, with no AST expression for the sandbox to intercept, so the cast can invoke
+ * arbitrary constructors (e.g. {@code ['secret.key'] -> new File('secret.key')}) outside the
+ * sandbox. We retype the loop variable to {@code Object} (so Groovy no longer emits the implicit
+ * cast) and prepend an explicit {@code v = (T) v} to the loop body.
+ *
+ *
It is invoked from {@link #visitForLoop} (not overridden in the {@link SandboxTransformer}
+ * subclass) so the injected {@link CastExpression} is in place before the body is visited by
+ * {@code getLoopBlock().visit(this)} at the end of {@code visitForLoop}; the subclass's
+ * {@code transform()} then rewrites it to a {@code Checker.checkedCast} like any other cast. A
+ * subclass override calling {@code super} first would run that visit before the cast existed,
+ * which would then need additional intervention to inject the {@code checkedCast} into the
+ * already-transformed body.
+ */
+ void rewriteForEachImplicitCast(ForStatement forLoop) {
+ Parameter variable = forLoop.getVariable();
+ ClassNode declaredType = variable.getOriginType();
+ if (declaredType == null || ClassHelper.isPrimitiveType(declaredType) || ClassHelper.OBJECT_TYPE.equals(declaredType)) {
+ return;
+ }
+ variable.setType(ClassHelper.OBJECT_TYPE);
+ variable.setOriginType(ClassHelper.OBJECT_TYPE);
+
+ CastExpression cast = new CastExpression(declaredType, new VariableExpression(variable));
+ BinaryExpression assignment = new BinaryExpression(new VariableExpression(variable), ASSIGNMENT_OP, cast);
+ assignment.setSourcePosition(variable);
+ ExpressionStatement assignmentStatement = new ExpressionStatement(assignment);
+ assignmentStatement.setSourcePosition(variable);
+
+ Statement body = forLoop.getLoopBlock();
+ BlockStatement newBody = new BlockStatement();
+ if (body instanceof BlockStatement) {
+ newBody.setVariableScope(((BlockStatement) body).getVariableScope());
+ }
+ newBody.addStatement(assignmentStatement);
+ newBody.addStatement(body);
+ newBody.setSourcePosition(body);
+ forLoop.setLoopBlock(newBody);
+ }
+
+ @Override
+ public void visitIfElse(IfStatement ifElse) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ ifElse.setBooleanExpression((BooleanExpression)transform(ifElse.getBooleanExpression()));
+ }
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ ifElse.getIfBlock().visit(this);
+ }
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ ifElse.getElseBlock().visit(this);
+ }
+ }
+
+ @Override
+ public void visitSwitch(SwitchStatement statement) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ super.visitSwitch(statement);
+ }
+ }
+
+ @Override
+ public void visitSynchronizedStatement(SynchronizedStatement sync) {
+ // Avoid super.visitSynchronizedStatement because it transforms the expression but then recurses on the entire
+ // SynchronizedStatement, causing the expression to be visited a second time.
+ sync.setExpression(transform(sync.getExpression()));
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ sync.getCode().visit(this);
+ }
+ }
+
+ @Override
+ public void visitTryCatchFinally(TryCatchStatement statement) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ super.visitTryCatchFinally(statement);
+ }
+ }
+
+ @Override
+ public void visitCatchStatement(CatchStatement statement) {
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ declareVariable(statement.getVariable());
+ super.visitCatchStatement(statement);
+ }
+ }
+
+ @Override
+ public void visitWhileLoop(WhileStatement loop) {
+ // Avoid super.visitWhileLoop because it transforms the boolean expression but then recurses on the entire
+ // WhileStatement, causing the boolean expression to be visited a second time.
+ loop.setBooleanExpression((BooleanExpression) transform(loop.getBooleanExpression()));
+ try (StackVariableSet scope = new StackVariableSet(this)) {
+ loop.getLoopBlock().visit(this);
+ }
+ }
+
+ /**
+ * @see org.codehaus.groovy.classgen.asm.BinaryExpressionHelper#evaluateEqual(org.codehaus.groovy.ast.expr.BinaryExpression, boolean)
+ */
+ void handleDeclarations(DeclarationExpression exp) {
+ Expression leftExpression = exp.getLeftExpression();
+ if (leftExpression instanceof VariableExpression) {
+ declareVariable((VariableExpression) leftExpression);
+ } else if (leftExpression instanceof TupleExpression) {
+ TupleExpression te = (TupleExpression) leftExpression;
+ for (Expression e : te.getExpressions()) {
+ declareVariable((VariableExpression)e);
+ }
+ }
+ }
+
+ void declareVariable(Variable exp) {
+ varScope.declare(exp.getName());
+ }
+}
diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/StackVariableSet.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/StackVariableSet.java
new file mode 100644
index 000000000..4c4cede1d
--- /dev/null
+++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/StackVariableSet.java
@@ -0,0 +1,48 @@
+package org.kohsuke.groovy.sandbox;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Keep track of in-scope variables on the stack.
+ *
+ * In groovy, various statements implicitly create new scopes (as in Java), so we track them
+ * in a chain.
+ *
+ * This only tracks variables on stack (as opposed to field access and closure accessing variables
+ * in the calling context.)
+ *
+ * @author Kohsuke Kawaguchi
+ */
+final class StackVariableSet implements AutoCloseable {
+
+ final ScopeTrackingClassCodeExpressionTransformer owner;
+ final StackVariableSet parent;
+
+ private final Set names = new HashSet<>();
+
+ StackVariableSet(ScopeTrackingClassCodeExpressionTransformer owner) {
+ this.owner = owner;
+ this.parent = owner.varScope;
+ owner.varScope = this;
+ }
+
+ void declare(String name) {
+ names.add(name);
+ }
+
+ /**
+ * Is the variable of the given name in scope?
+ */
+ boolean has(String name) {
+ for (StackVariableSet s=this; s!=null; s=s.parent)
+ if (s.names.contains(name))
+ return true;
+ return false;
+ }
+
+ @Override
+ public void close() {
+ owner.varScope = parent;
+ }
+}
diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Checker.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Checker.java
new file mode 100644
index 000000000..ab3753d17
--- /dev/null
+++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Checker.java
@@ -0,0 +1,1103 @@
+package org.kohsuke.groovy.sandbox.impl;
+
+import groovy.lang.Closure;
+import groovy.lang.EmptyRange;
+import groovy.lang.GString;
+import groovy.lang.GroovyRuntimeException;
+import groovy.lang.IntRange;
+import groovy.lang.MetaClass;
+import groovy.lang.MetaClassImpl;
+import groovy.lang.MetaMethod;
+import groovy.lang.MissingMethodException;
+import groovy.lang.MissingPropertyException;
+import groovy.lang.ObjectRange;
+
+import java.io.File;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.codehaus.groovy.classgen.asm.BinaryExpressionHelper;
+import org.codehaus.groovy.classgen.asm.UnaryExpressionHelper;
+import org.codehaus.groovy.reflection.ParameterTypes;
+import org.codehaus.groovy.runtime.InvokerHelper;
+import org.codehaus.groovy.runtime.MetaClassHelper;
+import org.codehaus.groovy.runtime.ResourceGroovyMethods;
+import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
+import org.codehaus.groovy.runtime.StringGroovyMethods;
+import org.codehaus.groovy.runtime.callsite.CallSite;
+import org.codehaus.groovy.runtime.callsite.CallSiteArray;
+import org.codehaus.groovy.syntax.Types;
+
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import static org.codehaus.groovy.runtime.InvokerHelper.getMetaClass;
+import static org.codehaus.groovy.runtime.MetaClassHelper.convertToTypeArray;
+import static org.kohsuke.groovy.sandbox.impl.ClosureSupport.BUILTIN_PROPERTIES;
+
+/**
+ * Intercepted Groovy script calls into this class.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class Checker {
+ private static final Object[] EMPTY_ARRAY = new Object[0];
+
+ /*TODO: specify the proper owner value*/
+ private static CallSite fakeCallSite(String method) {
+ CallSiteArray csa = new CallSiteArray(Checker.class, new String[]{method});
+ return csa.array[0];
+ }
+
+
+ // TODO: we need an owner class
+ public static Object checkedCall(Object _receiver, boolean safe, boolean spread, String _method, Object[] _args) throws Throwable {
+ if (safe && _receiver==null) return null;
+ _args = fixNull(_args);
+ if (spread) {
+ List