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: + * + *

+ * + * 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(...)") + *
  • Property access (e.g., z=foo.bar, z=foo."bar") and assignment (e.g., foo.bar=z, foo."bar"=z) + *
  • 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 r = new ArrayList(); + Iterator itr = InvokerHelper.asIterator(_receiver); + while (itr.hasNext()) { + Object it = itr.next(); + if (it!=null) + r.add(checkedCall(it, true, false, _method, _args)); + } + return r; + } else { + // the first try + // but this fails to properly intercept 5.class.forName('java.lang.String') +// def m = receiver.&"${method}"; +// return m(args) + +// from http://groovy.codehaus.org/Using+invokeMethod+and+getProperty + // but it still doesn't resolve static method +// def m = receiver.metaClass.getMetaMethod(method.toString(),args) +// return m.invoke(receiver,args); + + /* + When Groovy evaluates expression like "FooClass.bar()", it routes the call here. + (as to why we cannot rewrite the expression to statically route the call to checkedStaticCall, + consider "def x = FooClass.class; x.bar()", which still resolves to FooClass.bar() if it is present!) + + So this is where we really need to distinguish a call to a static method defined on the class + vs an instance method call to a method on java.lang.Class. + + Then the question is how do we know when to do which, which one takes precedence, etc. + Groovy doesn't commit to any specific logic at the level of MetaClass. In MetaClassImpl, + the logic is defined in MetaClassImpl.pickStaticMethod. + + BTW, this makes me wonder if StaticMethodCallExpression is used at all in AST, and it looks like + this is no longer used. + */ + + if (_receiver instanceof Class) { + Thunk maybeReplacement = findCheckedReplacement((Class)_receiver, _method, _args); + if (maybeReplacement != null) { + return maybeReplacement.call(); + } + + MetaClass mc = getMetaClass((Class) _receiver); + if (mc instanceof MetaClassImpl) { + MetaClassImpl mci = (MetaClassImpl) mc; + MetaMethod m = mci.retrieveStaticMethod(_method,_args); + if (m!=null) { + if (m.isStatic()) { + // Foo.forName() still finds Class.forName() method, so we need to test for that + if (m.getDeclaringClass().getTheClass()==Class.class) + return checkedStaticCall(Class.class,_method,_args); + else + return checkedStaticCall((Class)_receiver,_method,_args); + } + } + } + } + + if (_receiver instanceof Closure) { + if (_method.equals("invokeMethod") && isInvokingMethodOnClosure(_receiver,_method,_args)) { + // if someone is calling closure.invokeMethod("foo",args), map that back to closure.foo("args") + _method = _args[0].toString(); + _args = (Object[])_args[1]; + } + + MetaMethod m = getMetaClass(_receiver).pickMethod(_method, convertToTypeArray(_args)); + if (m==null) { + // if we are trying to call a method that's actually defined in Closure, then we'll get non-null 'm' + // in that case, treat it like normal method call + + // if we are here, that means we are trying to delegate the call to 'owner', 'delegate', etc. + // is going to, and check access accordingly. Groovy's corresponding code is in MetaClassImpl.invokeMethod(...) + List targets = ClosureSupport.targetsOf((Closure) _receiver); + + Class[] argTypes = convertToTypeArray(_args); + + // in the first phase, we look for exact method match + for (Object candidate : targets) { + if (InvokerHelper.getMetaClass(candidate).pickMethod(_method,argTypes)!=null) + return checkedCall(candidate,false,false, _method, _args); + } + // in the second phase, we try to call invokeMethod on them + for (Object candidate : targets) { + try { + return checkedCall(candidate,false,false,"invokeMethod",new Object[]{_method,_args}); + } catch (MissingMethodException e) { + // try the next one + } + } + // we tried to be smart about Closure.invokeMethod, but we are just not finding any. + // so we'll have to treat this like any other method. + } + } + + /* + The third try: + + Groovyc produces one CallSites instance per a call site, then + pack them into a single array and put them as a static field in a class. + this encapsulates the actual method dispatching logic. + + Ideally we'd like to get the CallSite object that would have been used for a call, + but because it's packed in an array and the index in that array is determined + only at the code generation time, I can't get the access to it. + + So here we are faking it by creating a new CallSite object. + */ + return new VarArgInvokerChain(_receiver) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) + return chain.next().onMethodCall(this,receiver,method,args); + else + return fakeCallSite(method).call(receiver,args); + } + }.call(_receiver,_method,_args); + } + } + + /** + * Are we trying to invoke a method defined on Closure or its super type? + * (If so, we'll need to chase down which method we are actually invoking.) + * + *

+ * Used for invokeMethod/getProperty/setProperty. + * + *

+ * If the receiver overrides this method, return false since we don't know how such methods behave. + */ + private static boolean isInvokingMethodOnClosure(Object receiver, String method, Object... args) { + if (receiver instanceof Closure) { + MetaMethod m = getMetaClass(receiver).pickMethod(method, convertToTypeArray(args)); + if (m!=null && m.getDeclaringClass().isAssignableFrom(Closure.class)) + return true; + } + return false; + } + + public static Object checkedStaticCall(Class _receiver, String _method, Object[] _args) throws Throwable { + _args = fixNull(_args); + Thunk maybeReplacement = findCheckedReplacement((Class)_receiver, _method, _args); + if (maybeReplacement != null) { + return maybeReplacement.call(); + } + return new VarArgInvokerChain(_receiver) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) + return chain.next().onStaticCall(this,(Class)receiver,method,args); + else + return fakeCallSite(method).callStatic((Class)receiver,args); + } + }.call(_receiver, _method, _args); + } + + public static Object checkedConstructor(Class _type, Object[] _args) throws Throwable { + // Make sure that this is not an illegal call to a synthetic constructor. + GroovyCallSiteSelector.findConstructor(_type, _args, null); + return new VarArgInvokerChain(_type) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) + return chain.next().onNewInstance(this,(Class)receiver,args); + else + // I believe the name is unused + return fakeCallSite("").callConstructor((Class)receiver,args); + } + }.call(_type,null,fixNull(_args)); + } + + public static Object checkedSuperCall(Class _senderType, Object _receiver, String _method, Object[] _args) throws Throwable { + Super s = new Super(_senderType, _receiver); + return new VarArgInvokerChain(s) { + public Object call(Object _s, String method, Object... args) throws Throwable { + Super s = (Super)_s; + if (chain.hasNext()) { + return chain.next().onSuperCall(this, s.senderType, s.receiver, method, args); + } else { + try { + MetaClass mc = InvokerHelper.getMetaClass(s.receiver.getClass()); + return mc.invokeMethod(s.senderType.getSuperclass(), s.receiver, method, args, true, true); + } catch (GroovyRuntimeException gre) { + throw ScriptBytecodeAdapter.unwrap(gre); + } + } + } + }.call(s,_method,fixNull(_args)); + } + + public static class SuperConstructorWrapper { + private final Object[] args; + SuperConstructorWrapper(Object[] args) { + this.args = args; + } + public Object arg(int idx) { + return args[idx]; + } + } + + public static SuperConstructorWrapper checkedSuperConstructor(Class thisClass, Class superClass, Object[] superCallArgs, Object[] constructorArgs, Class[] constructorParamTypes) throws Throwable { + // Make sure that the call to this synthetic constructor is not illegal. + GroovyCallSiteSelector.findConstructor(superClass, superCallArgs, SuperConstructorWrapper.class); + explicitConstructorCallSanity(thisClass, SuperConstructorWrapper.class, constructorArgs, constructorParamTypes); + new VarArgInvokerChain(superClass) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) { + chain.next().onSuperConstructor(this, superClass, args); + } + return null; + } + }.call(superClass, null, fixNull(superCallArgs)); + return new SuperConstructorWrapper(superCallArgs); + } + + public static class ThisConstructorWrapper { + private final Object[] args; + ThisConstructorWrapper(Object[] args) { + this.args = args; + } + public Object arg(int idx) { + return args[idx]; + } + } + + public static ThisConstructorWrapper checkedThisConstructor(final Class clazz, Object[] thisCallArgs, Object[] constructorArgs, Class[] constructorParamTypes) throws Throwable { + // Make sure that the call to this synthetic constructor is not illegal. + GroovyCallSiteSelector.findConstructor(clazz, thisCallArgs, ThisConstructorWrapper.class); + explicitConstructorCallSanity(clazz, ThisConstructorWrapper.class, constructorArgs, constructorParamTypes); + new VarArgInvokerChain(clazz) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) { + chain.next().onNewInstance(this, clazz, args); + } + return null; + } + }.call(clazz, null, fixNull(thisCallArgs)); + return new ThisConstructorWrapper(thisCallArgs); + } + + /** + * Makes sure that explicit constructor calls inside of synthetic constructors will go to the intended constructor + * at runtime (Part of SECURITY-1754). + * See {@code SandboxTransformerTest.blocksUnintendedCallsToNonSyntheticConstructors()} for an example of this problem. + */ + private static void explicitConstructorCallSanity(Class thisClass, Class wrapperClass, Object[] argsExcludingWrapper, Class[] paramsIncludingWrapper) { + // Construct argument types for the explicit constructor call. + Class[] argTypes = new Class[argsExcludingWrapper.length + 1]; + argTypes[0] = wrapperClass; + System.arraycopy(MetaClassHelper.convertToTypeArray(argsExcludingWrapper), 0, argTypes, 1, argsExcludingWrapper.length); + // Find the constructor that the sandbox is expecting will be called. + Constructor expectedConstructor = null; + try { + expectedConstructor = thisClass.getDeclaredConstructor(paramsIncludingWrapper); + } catch (NoSuchMethodException e) { + // The original constructor that made it necessary to create a synthetic constructor should always exist. + throw new AssertionError("Unable to find original constructor", e); + } + ParameterTypes expectedParamTypes = new ParameterTypes(paramsIncludingWrapper); + for (Constructor c : thisClass.getDeclaredConstructors()) { + // Make sure that no other constructor matches the arguments better than the constructor we are expecting to + // call, because otherwise that would be the constructor that would actually be invoked. + ParameterTypes cParamTypes = new ParameterTypes(c.getParameterTypes()); + if (!c.equals(expectedConstructor) && cParamTypes.isValidMethod(argTypes) && GroovyCallSiteSelector.isMoreSpecific(cParamTypes, expectedParamTypes, argTypes)) { + throw new SecurityException("Rejecting unexpected invocation of constructor: " + c + ". Expected to invoke synthetic constructor: " + expectedConstructor); + } + } + } + + public static Object checkedGetProperty(final Object _receiver, boolean safe, boolean spread, Object _property) throws Throwable { + if (safe && _receiver==null) return null; + + if (spread || (_receiver instanceof Collection && !BUILTIN_PROPERTIES.contains(_property))) { + List r = new ArrayList(); + Iterator itr = InvokerHelper.asIterator(_receiver); + while (itr.hasNext()) { + Object it = itr.next(); + if (it!=null) + r.add(checkedGetProperty(it,true,false,_property)); + } + return r; + } +// 1st try: do the same call site stuff +// return fakeCallSite(property.toString()).callGetProperty(receiver); + + if (isInvokingMethodOnClosure(_receiver, "getProperty", _property) && !BUILTIN_PROPERTIES.contains(_property)) { + // if we are trying to invoke Closure.getProperty(), + // we want to find out where the call is going to, and check that target + MissingPropertyException x=null; + for (Object candidate : ClosureSupport.targetsOf((Closure) _receiver)) { + try { + return checkedGetProperty(candidate, false, false, _property); + } catch (MissingPropertyException e) { + x = e; + // try the next one + } + } + if (x!=null) throw x; + throw new MissingPropertyException(_property.toString(), _receiver.getClass()); + } + if (_receiver instanceof Map) { + /* + MetaClassImpl.getProperty looks for Map subtype and handles it as Map.get call, + so dispatch that call accordingly. + */ + return checkedCall(_receiver,false,false,"get",new Object[]{_property}); + } + + return new ZeroArgInvokerChain(_receiver) { + public Object call(Object receiver, String property) throws Throwable { + if (chain.hasNext()) + return chain.next().onGetProperty(this,receiver,property); + else + return ScriptBytecodeAdapter.getProperty(null, receiver, property); + } + }.call(_receiver,_property.toString()); + } + + public static Object checkedSetProperty(Object _receiver, Object _property, boolean safe, boolean spread, int op, Object _value) throws Throwable { + if (op!=Types.ASSIGN) { + // a compound assignment operator is decomposed into get+op+set + // for example, a.x += y => a.x=a.x+y + Object v = checkedGetProperty(_receiver, safe, spread, _property); + return checkedSetProperty(_receiver, _property, safe, spread, Types.ASSIGN, + checkedBinaryOp(v, Ops.compoundAssignmentToBinaryOperator(op), _value)); + } + if (safe && _receiver==null) return _value; + if (spread) { + Iterator itr = InvokerHelper.asIterator(_receiver); + while (itr.hasNext()) { + Object it = itr.next(); + if (it!=null) + checkedSetProperty(it, _property, true, false, op, _value); + } + return _value; + } + + if (isInvokingMethodOnClosure(_receiver, "setProperty", _property, _value) && !BUILTIN_PROPERTIES.contains(_property)) { + // if we are trying to invoke Closure.setProperty(), + // we want to find out where the call is going to, and check that target + GroovyRuntimeException x=null; + for (Object candidate : ClosureSupport.targetsOf((Closure) _receiver)) { + try { + return checkedSetProperty(candidate, _property, false, false, op, _value); + } catch (GroovyRuntimeException e) { + // Cathing GroovyRuntimeException feels questionable, but this is how Groovy does it in + // Closure.setPropertyTryThese(). + x = e; + // try the next one + } + } + if (x!=null) + throw x; + throw new MissingPropertyException(_property.toString(), _receiver.getClass()); + } + if (_receiver instanceof Map) { + /* + MetaClassImpl.setProperty looks for Map subtype and handles it as Map.put call, + so dispatch that call accordingly. + */ + checkedCall(_receiver,false,false,"put",new Object[]{_property,_value}); + return _value; + } + + return new SingleArgInvokerChain(_receiver) { + public Object call(Object receiver, String property, Object value) throws Throwable { + if (chain.hasNext()) + return chain.next().onSetProperty(this,receiver,property,value); + else { + // according to AsmClassGenerator this is how the compiler maps it to + // TODO: There is an implicit cast here. Very awkward for us to handle because we have to fully + // understand the meaning of receiver.property to know the target type of the cast. + // For now, API consumers must handle it themselves in onSetProperty. + ScriptBytecodeAdapter.setProperty(value,null,receiver,property); + return value; + } + } + }.call(_receiver,_property.toString(),_value); + } + + public static Object checkedGetAttribute(Object _receiver, boolean safe, boolean spread, Object _property) throws Throwable { + if (safe && _receiver==null) return null; + if (spread) { + List r = new ArrayList(); + Iterator itr = InvokerHelper.asIterator(_receiver); + while (itr.hasNext()) { + Object it = itr.next(); + if (it!=null) + r.add(checkedGetAttribute(it, true, false, _property)); + } + return r; + } else { + return new ZeroArgInvokerChain(_receiver) { + public Object call(Object receiver, String property) throws Throwable { + if (chain.hasNext()) + return chain.next().onGetAttribute(this,receiver,property); + else + // according to AsmClassGenerator this is how the compiler maps it to + return ScriptBytecodeAdapter.getField(null,receiver,property); + } + }.call(_receiver,_property.toString()); + } + } + + /** + * Intercepts the attribute assignment of the form "receiver.@property = value" + * + * @param op + * One of the assignment operators of {@link Types} + */ + public static Object checkedSetAttribute(Object _receiver, Object _property, boolean safe, boolean spread, int op, Object _value) throws Throwable { + if (op!=Types.ASSIGN) { + // a compound assignment operator is decomposed into get+op+set + // for example, a.@x += y => a.@x=a.@x+y + Object v = checkedGetAttribute(_receiver, safe, spread, _property); + return checkedSetAttribute(_receiver, _property, safe, spread, Types.ASSIGN, + checkedBinaryOp(v, Ops.compoundAssignmentToBinaryOperator(op), _value)); + } + if (safe && _receiver==null) return _value; + if (spread) { + Iterator itr = InvokerHelper.asIterator(_receiver); + while (itr.hasNext()) { + Object it = itr.next(); + if (it!=null) + checkedSetAttribute(it,_property,true,false,op,_value); + } + } else { + return new SingleArgInvokerChain(_receiver) { + public Object call(Object receiver, String property, Object value) throws Throwable { + if (chain.hasNext()) + return chain.next().onSetAttribute(this,receiver,property,value); + else { + ScriptBytecodeAdapter.setField(value,null,receiver,property); + return value; + } + } + }.call(_receiver,_property.toString(),_value); + } + return _value; + } + + public static Object checkedGetArray(Object _receiver, Object _index) throws Throwable { + return new SingleArgInvokerChain(_receiver) { + public Object call(Object receiver, String method, Object index) throws Throwable { + if (chain.hasNext()) + return chain.next().onGetArray(this,receiver,index); + else + // BinaryExpressionHelper.eval maps this to "getAt" call + return fakeCallSite("getAt").call(receiver,index); + } + }.call(_receiver,null,_index); + } + + /** + * Intercepts the array assignment of the form "receiver[index] = value" + * + * @param op + * One of the assignment operators of {@link Types} + */ + public static Object checkedSetArray(Object _receiver, Object _index, int op, Object _value) throws Throwable { + if (op!=Types.ASSIGN) { + // a compound assignment operator is decomposed into get+op+set + // for example, a[x] += y => a[x]=a[x]+y + Object v = checkedGetArray(_receiver, _index); + return checkedSetArray(_receiver, _index, Types.ASSIGN, + checkedBinaryOp(v, Ops.compoundAssignmentToBinaryOperator(op), _value)); + } else { + // Note that in regular Groovy, value is cast to the component type of the array, but this code does not do that. + return new TwoArgInvokerChain(_receiver) { + public Object call(Object receiver, String method, Object index, Object value) throws Throwable { + if (chain.hasNext()) + return chain.next().onSetArray(this,receiver,index,value); + else { + // BinaryExpressionHelper.assignToArray maps this to "putAt" call + fakeCallSite("putAt").call(receiver,index,value); + return value; + } + } + }.call(_receiver,null,_index,_value); + } + } + + /** + * a[i]++ / a[i]-- + * + * @param op + * "next" for ++, "previous" for --. These names are defined by Groovy. + */ + public static Object checkedPostfixArray(Object r, Object i, String op) throws Throwable { + Object o = checkedGetArray(r, i); + Object n = checkedCall(o, false, false, op, new Object[0]); + checkedSetArray(r,i,Types.ASSIGN,n); + return o; + } + + /** + * ++a[i] / --a[i] + */ + public static Object checkedPrefixArray(Object r, Object i, String op) throws Throwable { + Object o = checkedGetArray(r, i); + Object n = checkedCall(o, false, false, op, new Object[0]); + checkedSetArray(r,i,Types.ASSIGN,n); + return n; + } + + /** + * a.x++ / a.x-- + */ + public static Object checkedPostfixProperty(Object receiver, Object property, boolean safe, boolean spread, String op) throws Throwable { + Object o = checkedGetProperty(receiver, safe, spread, property); + Object n = checkedCall(o, false, false, op, new Object[0]); + checkedSetProperty(receiver, property, safe, spread, Types.ASSIGN, n); + return o; + } + + /** + * ++a.x / --a.x + */ + public static Object checkedPrefixProperty(Object receiver, Object property, boolean safe, boolean spread, String op) throws Throwable { + Object o = checkedGetProperty(receiver, safe, spread, property); + Object n = checkedCall(o, false, false, op, new Object[0]); + checkedSetProperty(receiver, property, safe, spread, Types.ASSIGN, n); + return n; + } + + /** + * a.@x++ / a.@x-- + */ + public static Object checkedPostfixAttribute(Object receiver, Object property, boolean safe, boolean spread, String op) throws Throwable { + Object o = checkedGetAttribute(receiver, safe, spread, property); + Object n = checkedCall(o, false, false, op, new Object[0]); + checkedSetAttribute(receiver, property, safe, spread, Types.ASSIGN, n); + return o; + } + + /** + * ++a.@x / --a.@x + */ + public static Object checkedPrefixAttribute(Object receiver, Object property, boolean safe, boolean spread, String op) throws Throwable { + Object o = checkedGetAttribute(receiver, safe, spread, property); + Object n = checkedCall(o, false, false, op, new Object[0]); + checkedSetAttribute(receiver, property, safe, spread, Types.ASSIGN, n); + return n; + } + + /** + * Intercepts the binary expression of the form {@code lhs op rhs} like {@code lhs+rhs}, {@code lhs>>rhs}, etc. + * + * In Groovy, binary operators are method calls. + * + * @param op + * One of the binary operators of {@link Types} + * @see BinaryExpressionHelper#evaluateBinaryExpressionWithAssignment + */ + public static Object checkedBinaryOp(Object lhs, int op, Object rhs) throws Throwable { + return checkedCall(lhs,false,false,Ops.binaryOperatorMethods(op),new Object[]{rhs}); + } + + /** + * Intercepts unary expressions of the form {@code ~value}. + * + * In Groovy, this operator may result in a call to a method named {@code bitwiseNegate} on the receiver or to one + * of the {@code DefaultGroovyMethods.bitwiseNegate} overloads. + * + * @see UnaryExpressionHelper#writeBitwiseNegate + * @see ScriptBytecodeAdapter#bitwiseNegate + * @see InvokerHelper#bitwiseNegate + */ + public static Object checkedBitwiseNegate(Object value) throws Throwable { + if (value instanceof Integer) { + return ~((Integer)value); + } + if (value instanceof Long) { + return ~((Long)value); + } + if (value instanceof BigInteger) { + return checkedCall(value, false, false, "not", new Object[]{}); + } + if (value instanceof String) { + // value is a regular expression. + return checkedStaticCall(StringGroovyMethods.class, "bitwiseNegate", new Object[]{ value.toString() }); + } + if (value instanceof GString) { + // value is a regular expression. + return checkedStaticCall(StringGroovyMethods.class, "bitwiseNegate", new Object[]{ value.toString() }); + } + if (value instanceof ArrayList) { // ArrayList is the exact type that Groovy checks in InvokerHelper.bitwiseNegate. + // value is a list. + List newlist = new ArrayList(); + for (Object element : ((ArrayList) value)) { + newlist.add(checkedBitwiseNegate(element)); + } + return newlist; + } + return checkedCall(value, false, false, "bitwiseNegate", EMPTY_ARRAY); + } + + /** + * Intercepts range expressions of the form {@code [x..y]} or {@code [x.. clazz, Object exp, boolean ignoreAutoboxing, boolean coerce, boolean strict) throws Throwable { + return preCheckedCast(clazz, exp, ignoreAutoboxing, coerce, strict).call(); + } + + /** Same as {@link Callable} but can throw {@link Throwable}. */ + @FunctionalInterface + public interface Thunk { + Object call() throws Throwable; + } + + public static Thunk preCheckedCast(Class clazz, Object exp, boolean ignoreAutoboxing, boolean coerce, boolean strict) throws Throwable { + // Note: Be careful calling methods on exp here since the user has control over that object. + if (exp != null && + // Ignore some things handled by DefaultGroovyMethods.asType(Collection, Class), e.g., `[1, 2, 3] as Set` (interface → first clause) or `[1, 2, 3] as HashSet` (collection assigned to concrete class → second clause): + !(Collection.class.isAssignableFrom(clazz) && clazz.getPackage().getName().equals("java.util"))) { + // Don't actually cast at all if this is already assignable. + if (clazz.isAssignableFrom(exp.getClass())) { + return () -> exp; + } else if (clazz.isInterface()) { + for (Method m : clazz.getMethods()) { + Object[] args = new Object[m.getParameterTypes().length]; + for (int i = 0; i < args.length; i++) { + args[i] = getDefaultValue(m.getParameterTypes()[i]); + } + // We intercept all methods defined on the interface to ensure they are permitted, and deliberately ignore the return value: + new VarArgInvokerChain(exp) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) { + if (receiver instanceof Class) { + return chain.next().onStaticCall(this, (Class) receiver, method, args); + } else { + return chain.next().onMethodCall(this, receiver, method, args); + } + } else { + return null; + } + } + }.call(exp, m.getName(), args); + } + } else if (Modifier.isAbstract(clazz.getModifiers()) && !Modifier.isFinal(clazz.getModifiers()) && (exp instanceof Closure || exp instanceof Map)) { + // Groovy will create a proxy object whose methods will delegate to the closure or map values. + // The bodies of any closures cast using this mechanism will be be sandbox transformed, but we check + // whether the abstract class is allowed to be instantiated in the sandbox as a precaution. + // Technically, if coerce is false, then this should only happen if the abstract class has a single + // abstract method, but it seems simplest to handle the cases symmetrically and risk a false positive + // RejectedAccessException in some cases that would throw a GroovyCastException in regular Groovy. + for (Constructor c : clazz.getConstructors()) { // ProxyGeneratorAdapter seems to generate a constructor for each constructor in the abstract class, and I am not sure which one will be used, so we intercept them all. + Object[] args = new Object[c.getParameterTypes().length]; + for (int i = 0; i < args.length; i++) { + args[i] = getDefaultValue(c.getParameterTypes()[i]); + } + new VarArgInvokerChain(exp) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) { + return chain.next().onNewInstance(this, clazz, args); + } else { + return null; + } + } + }.call(clazz, null, args); + } + } else if ((clazz == boolean.class || clazz == Boolean.class) && exp.getClass() != Boolean.class) { + // Boolean casts must never be handled as constructor invocation. + new ZeroArgInvokerChain(exp) { + public Object call(Object receiver, String method) throws Throwable { + if (chain.hasNext()) { + return chain.next().onMethodCall(this, receiver, method); + } else { + return null; + } + } + }.call(exp, "asBoolean"); + } else if (unbox(clazz).isPrimitive() || clazz == String.class) { + // Casts to non-boolean primitives (and their boxed equivalents) and to String never + // perform any reflective operations, so we do not care about them, and they should never be handled as + // constructor invocation. + } else if (!clazz.isArray() && clazz != Object.class && !Modifier.isAbstract(clazz.getModifiers()) && (exp instanceof Collection || exp.getClass().isArray() || exp instanceof Map)) { + Object[] args = null; + if (exp instanceof Collection) { + if (isCollectionSafeToCast((Collection) exp)) { + args = ((Collection) exp).toArray(); + } else { + throw new UnsupportedOperationException( + "Casting non-standard Collections to a type via constructor is not supported. " + + "Consider converting " + exp.getClass() + " to a Collection defined in the java.util package and then casting to " + clazz + "."); + } + } else if (exp instanceof Map) { + args = new Object[] {exp}; + } else { // arrays + // TODO tricky to determine which constructor will actually be called; array might be expanded, or might not + throw new UnsupportedOperationException("casting arrays to types via constructor is not yet supported"); + } + if (args != null) { + // We intercept the constructor that will be used for the cast, and again, deliberately ignore the return value: + new VarArgInvokerChain(clazz) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) { + return chain.next().onNewInstance(this, (Class) receiver, args); + } else { + return null; + } + } + }.call(clazz, null, args); + } else { + throw new IllegalStateException(exp.getClass() + ".toArray() must not return null"); + } + } else if (clazz.isArray() && !clazz.getComponentType().isPrimitive() && (exp instanceof Collection || exp instanceof Object[])) { + Object[] array; + if (exp instanceof Collection) { + if (isCollectionSafeToCast((Collection) exp)) { + array = ((Collection) exp).toArray(); + } else { + throw new UnsupportedOperationException( + "Casting non-standard implementations of Collection to an array is not supported. " + + "Consider converting " + exp.getClass() + " to a Collection defined in the java.util package and then casting to " + clazz + "."); + } + } else { + array = (Object[])exp; + } + // We intercept the per-element casts. + for (Object element : array) { + preCheckedCast(clazz.getComponentType(), element, coerce, strict, ignoreAutoboxing); + } + } else if (clazz == File.class && exp instanceof CharSequence) { + Object[] args = new Object[]{exp.toString()}; + // We intercept the constructor that will be used for the cast, and again, deliberately ignore the return value: + new VarArgInvokerChain(clazz) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext()) { + return chain.next().onNewInstance(this, (Class) receiver, args); + } else { + return null; + } + } + }.call(clazz, null, args); + } else if (exp instanceof File && (clazz.isArray() || Collection.class.isAssignableFrom(clazz))) { + // see https://github.com/apache/groovy/blob/edcd6c4435138733668cd75ac0d3342efb39dc05/src/main/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java#L472-L479 + // We intercept the method that will be used for the cast, and again, deliberately ignore the return value: + new VarArgInvokerChain(clazz) { + public Object call(Object receiver, String method, Object... args) throws Throwable { + if (chain.hasNext() && receiver instanceof Class) { + return chain.next().onStaticCall(this, (Class) receiver, method, args); + } else { + return null; + } + } + }.call(ResourceGroovyMethods.class, "readLines", exp); + } else if (exp instanceof Class && ((Class) exp).isEnum() && (clazz.isArray() || Collection.class.isAssignableFrom(clazz))) { + // see https://github.com/apache/groovy/blob/edcd6c4435138733668cd75ac0d3342efb39dc05/src/main/org/codehaus/groovy/runtime/typehandling/DefaultTypeTransformation.java#L480-L483 + for (Field f : ((Class) exp).getFields()) { + if (f.isEnumConstant()) { + // We intercept all Enum constants to ensure they are permitted, and deliberately ignore the return value: + new ZeroArgInvokerChain(exp) { + public Object call(Object receiver, String field) throws Throwable { + if (chain.hasNext() && receiver instanceof Class) { + return chain.next().onGetProperty(this, receiver, field); + } else { + return null; + } + } + }.call(exp, f.getName()); + } + } + } + } + // TODO what does ignoreAutoboxing do? + return () -> strict ? clazz.cast(exp) : coerce ? ScriptBytecodeAdapter.asType(exp, clazz) : ScriptBytecodeAdapter.castToType(exp, clazz); + } + // https://stackoverflow.com/a/38243203/12916 + @SuppressWarnings("unchecked") + private static T getDefaultValue(Class clazz) { + return (T) Array.get(Array.newInstance(clazz, 1), 0); + } + + /** + * Issue #2 revealed that Groovy can call methods with null in the var-arg array, + * when it should be passing an Object array of length 1 with null value. + */ + private static Object[] fixNull(Object[] args) { + return args==null ? new Object[1] : args; + } + + /** + * When casting collections to types via constructor, we cannot allow user-defined implementations of {@link Collection}. + * This is because a user-defined implementation of {@link Collection} can do tricky things to return a different + * set of elements for {@link Collection#toArray} inside of {@link #preCheckedCast} than whatever + * {@link ScriptBytecodeAdapter#asType} ends up using as the elements, so we are not able to guarantee that the + * Constructor we pre-checked is the one that will end up being invoked. + */ + private static boolean isCollectionSafeToCast(Collection c) { + Package p = c.getClass().getPackage(); + String packageName = null; + if (p != null) { + packageName = p.getName(); + } + // TODO: Are there any other packages with collections that we should allow? + return "java.util".equals(packageName); + } + + /** + * Look in {@link #GROOVY_RUNTIME_REPLACEMENTS} to see if {@link Checker} defines a checked equivalent of the given + * method, and if so, return a {@link Thunk} that will invoke the checked method rather than the original. + * + *

Groovy uses runtime APIs (e.g. {@link ScriptByteCodeAdapter}) to support various standard language features + * such as unary operators. Some of these APIs invoke methods reflectively based on the runtime types of arguments, + * which the sandbox does not see and so it cannot intercept those calls. We define sandbox-aware replacements for + * these methods so that these reflective calls can be intercepted. + *

When using groovy-sandbox through script-security without groovy-cps, these replacements only take effect if + * a script directly calls one of the original methods. When using groovy-cps, these replacements also take effect + * when their corresponding AST nodes (e.g. unary operators) are used. + */ + private static Thunk findCheckedReplacement(Class clazz, String method, Object[] args) { + Method maybeReplacement = GROOVY_RUNTIME_REPLACEMENTS.get(new SimpleImmutableEntry(clazz, method)); + if (maybeReplacement == null) { + return null; + } + ParameterTypes parameterTypes = new ParameterTypes(maybeReplacement.getParameterTypes()); + if (!parameterTypes.isValidExactMethod(args)) { + return null; + } + return () -> { + try { + return maybeReplacement.invoke(null, args); + } catch (InvocationTargetException e) { + throw e.getCause(); // e.g. CpsCallableInvocation + } + }; + } + + /** + * A map from Groovy methods to checked replacements that will be used instead when the sandbox is active. + */ + private static final HashMap, String>, Method> GROOVY_RUNTIME_REPLACEMENTS = new HashMap<>(); + static { + addReplacement(InvokerHelper.class, "bitwiseNegate", "checkedBitwiseNegate", Object.class); + addReplacement(InvokerHelper.class, "unaryMinus", "checkedUnaryMinus", Object.class); + addReplacement(InvokerHelper.class, "unaryPlus", "checkedUnaryPlus", Object.class); + addReplacement(ScriptBytecodeAdapter.class, "bitwiseNegate", "checkedBitwiseNegate", Object.class); + addReplacement(ScriptBytecodeAdapter.class, "unaryMinus", "checkedUnaryMinus", Object.class); + addReplacement(ScriptBytecodeAdapter.class, "unaryPlus", "checkedUnaryPlus", Object.class); + addReplacement(ScriptBytecodeAdapter.class, "createRange", "checkedCreateRange", Object.class, Object.class, boolean.class); + } + + private static void addReplacement(Class clazz, String name, String checkedName, Class... parameterTypes) { + try { + GROOVY_RUNTIME_REPLACEMENTS.put(new SimpleImmutableEntry(clazz, name), Checker.class.getDeclaredMethod(checkedName, parameterTypes)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); // Developer error. + } + } + + private static Class unbox(Class clazz) { + return BOX_TO_PRIMITIVE.getOrDefault(clazz, clazz); + } + + private static final Map, Class> BOX_TO_PRIMITIVE = new HashMap<>(); + static { + BOX_TO_PRIMITIVE.put(Boolean.class, boolean.class); + BOX_TO_PRIMITIVE.put(Byte.class, byte.class); + BOX_TO_PRIMITIVE.put(Character.class, char.class); + BOX_TO_PRIMITIVE.put(Double.class, double.class); + BOX_TO_PRIMITIVE.put(Float.class, float.class); + BOX_TO_PRIMITIVE.put(Integer.class, int.class); + BOX_TO_PRIMITIVE.put(Long.class, long.class); + BOX_TO_PRIMITIVE.put(Short.class, short.class); + } +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/ClosureSupport.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/ClosureSupport.java new file mode 100644 index 000000000..796765074 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/ClosureSupport.java @@ -0,0 +1,74 @@ +package org.kohsuke.groovy.sandbox.impl; + +import groovy.lang.Closure; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Helps with sanbox intercepting Closures, which has unique dispatching rules we need to understand. + * + * @author Kohsuke Kawaguchi + */ +final class ClosureSupport { + /** + * {@link Closure} forwards methods/properties to other objects, depending on the resolution strategy. + *

+ * This method returns the list of non-null objects that should be considered, in that order. + */ + public static List targetsOf(Closure receiver) { + Object owner = receiver.getOwner(); + Object delegate = receiver.getDelegate(); + + // Groovy's method dispatch logic for Closure is defined in MetaClassImpl.invokeMethod + switch (receiver.getResolveStrategy()) { + case Closure.OWNER_FIRST: + return of(owner,delegate); + case Closure.DELEGATE_FIRST: + return of(delegate,owner); + case Closure.OWNER_ONLY: + return of(owner); + case Closure.DELEGATE_ONLY: + return of(delegate); + case Closure.TO_SELF: + default: + // fields/methods defined on Closure are checked by SandboxInterceptor, + // so if we are here it means we will not find the target of the dispatch. + return Collections.emptyList(); + } + } + + private static List of(Object o1, Object o2) { + // various cases where the list of two become the list of one (or empty) + if (o1==null) return of(o2); + if (o2==null) return of(o1); + if (o1==o2) return of(o1); + + return Arrays.asList(o1, o2); + } + + private static List of(Object maybeNull) { + if (maybeNull==null) + return Collections.emptyList(); + return Collections.singletonList(maybeNull); + } + + /** + * Built-in properties on {@link Closure} that do not follow the delegation rules. + */ + public static final Set BUILTIN_PROPERTIES = new HashSet(Arrays.asList( + "delegate", + "owner", + "maximumNumberOfParameters", + "parameterTypes", + "metaClass", + "class", + "directive", + "resolveStrategy", + "thisObject" + )); + +} \ No newline at end of file diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelector.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelector.java new file mode 100644 index 000000000..7098ae368 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelector.java @@ -0,0 +1,154 @@ +/* + * The MIT License + * + * Copyright 2020 CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox.impl; + +import java.lang.reflect.Constructor; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.codehaus.groovy.reflection.ParameterTypes; +import org.codehaus.groovy.runtime.MetaClassHelper; + +public class GroovyCallSiteSelector { + + private GroovyCallSiteSelector() {} + + /** + * Find the {@link Constructor} that Groovy will invoke at runtime for the given type and arguments. + * + * @throws SecurityException if no valid constructor is found, or if the constructor is a synthetic constructor + * added by SandboxTransformer and the constructor wrapper argument is invalid. + */ + public static Constructor findConstructor(Class type, Object[] args, Class expectedConstructorWrapper) { + Constructor c = constructor(type, args); + if (c == null) { + throw new SecurityException("Unable to find constructor: " + GroovyCallSiteSelector.formatConstructor(type, args)); + } + // Check to make sure that users are not directly calling synthetic constructors without going through + // `Checker.checkedSuperConstructor` or `Checker.checkedThisConstructor`. Part of SECURITY-1754. + if (isSandboxGeneratedConstructor(c) && ( + expectedConstructorWrapper == null || // Generated constructors should never be called directly, so any call from Checker.checkedConstructor should be rejected + args.length < 1 || // Should always be false since isSandboxGeneratedConstructor returned true + args[0] == null || // The wrapper argument must not be null + args[0].getClass() != expectedConstructorWrapper)) { // The first argument must match the expected wrapper type + String alternateConstructors = Stream.of(c.getDeclaringClass().getDeclaredConstructors()) + .filter(tempC -> !isSandboxGeneratedConstructor(tempC)) + .map(Object::toString) + .sorted() + .collect(Collectors.joining(", ")); + throw new SecurityException("Rejecting illegal call to synthetic constructor: " + c + ". Perhaps you meant to use one of these constructors instead: " + alternateConstructors); + } + return c; + } + + static Constructor constructor(Class receiver, Object[] args) { + Constructor[] constructors = receiver.getDeclaredConstructors(); + Constructor bestMatch = null; + ParameterTypes bestMatchParamTypes = null; + Class[] argTypes = MetaClassHelper.convertToTypeArray(args); + for (Constructor c : constructors) { + ParameterTypes cParamTypes = new ParameterTypes(c.getParameterTypes()); + if (cParamTypes.isValidMethod(argTypes)) { + if (bestMatch == null || isMoreSpecific(cParamTypes, bestMatchParamTypes, argTypes)) { + bestMatch = c; + bestMatchParamTypes = cParamTypes; + } + } + } + if (bestMatch != null) { + return bestMatch; + } + + // Only check for the magic Map constructor if we haven't already found a real constructor. + // Also note that this logic is derived from how Groovy itself decides to use the magic Map constructor, at + // MetaClassImpl#invokeConstructor(Class, Object[]). + if (args.length == 1 && args[0] instanceof Map) { + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 0 && !c.isVarArgs()) { + return c; + } + } + } + + return null; + } + + public static boolean isMoreSpecific(ParameterTypes paramsForCandidate, ParameterTypes paramsForBaseline, Class[] argTypes) { + long candidateDistance = MetaClassHelper.calculateParameterDistance(argTypes, paramsForCandidate); + long currentBestDistance = MetaClassHelper.calculateParameterDistance(argTypes, paramsForBaseline); + return candidateDistance < currentBestDistance; + } + + private static final Class[] SYNTHETIC_CONSTRUCTOR_PARAMETER_TYPES = new Class[] { + Checker.SuperConstructorWrapper.class, + Checker.ThisConstructorWrapper.class, + }; + + /** + * @return true if this constructor is one that was added by groovy-sandbox in {@code SandboxTransformer.processConstructors} + * specifically to be able to intercept calls to super in constructors. + */ + private static boolean isSandboxGeneratedConstructor(Constructor c) { + if (!c.isSynthetic()) { + return false; + } + Class[] parameterTypes = c.getParameterTypes(); + if (parameterTypes.length > 0) { + for (Class syntheticParamType : SYNTHETIC_CONSTRUCTOR_PARAMETER_TYPES) { + if (parameterTypes[0] == syntheticParamType) { + return true; + } + } + } + return false; + } + + public static String formatConstructor(Class c, Object... args) { + return "new " + getName(c) + printArgumentTypes(args); + } + + private static String printArgumentTypes(Object[] args) { + StringBuilder b = new StringBuilder(); + for (Object arg : args) { + b.append(' '); + b.append(getName(arg)); + } + return b.toString(); + } + + public static String getName(Object o) { + return o == null ? "null" : getName(o.getClass()); + } + + private static String getName(Class c) { + Class e = c.getComponentType(); + if (e == null) { + return c.getName(); + } else { + return getName(e) + "[]"; + } + } + +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/InvokerChain.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/InvokerChain.java new file mode 100644 index 000000000..4c7f0968d --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/InvokerChain.java @@ -0,0 +1,38 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor; +import org.kohsuke.groovy.sandbox.GroovyInterceptor.Invoker; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * @author Kohsuke Kawaguchi + */ +abstract class InvokerChain implements Invoker { + protected final Iterator chain; + + protected InvokerChain(Object receiver) { + // See issue #6, #15. When receiver is null, technically speaking Groovy handles this + // as if NullObject.INSTANCE is the receiver. OTOH, it's confusing + // to GroovyInterceptor that the receiver can be null, so I'm + // bypassing the checker in this case. + if (receiver==null) { + chain = EMPTY_ITERATOR; + } else { + List interceptors = GroovyInterceptor.getApplicableInterceptors(); + if (interceptors.isEmpty()) { + // We are running sandbox-transformed code, but there is no interceptor on the current thread. + // This is dangerous (SECURITY-2020), so we reject everything. + chain = REJECT_EVERYTHING.iterator(); + } else { + chain = interceptors.iterator(); + } + } + } + + private static final Iterator EMPTY_ITERATOR = Collections.emptyList().iterator(); + private static final Set REJECT_EVERYTHING = Collections.singleton(new RejectEverythingInterceptor()); +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Ops.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Ops.java new file mode 100644 index 000000000..4bf4e5b9e --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Ops.java @@ -0,0 +1,91 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.codehaus.groovy.syntax.Types; + +import java.util.HashMap; +import java.util.Map; + +import static org.codehaus.groovy.syntax.Types.*; + +/** + * Additional relationship between operators. + * + * @author Kohsuke Kawaguchi + * @see Types + */ +public class Ops { + private static final Map compoundAssignmentToBinaryOperator = new HashMap(); + + public static int compoundAssignmentToBinaryOperator(int type) { + Integer o = compoundAssignmentToBinaryOperator.get(type); + if (o==null) throw new IllegalArgumentException(""+type); + return o; + } + + private static final Map binaryOperatorMethods = new HashMap(); + + public static String binaryOperatorMethods(int type) { + String v = binaryOperatorMethods.get(type); + if (v==null) throw new IllegalArgumentException(""+type); + return v; + } + + public static boolean isComparisionOperator(int type) { + return Types.ofType(type,COMPARISON_OPERATOR); + } + + public static boolean isRegexpComparisonOperator(int type) { + return Types.ofType(type,REGEX_COMPARISON_OPERATOR); + } + + public static boolean isLogicalOperator(int type) { + return Types.ofType(type,LOGICAL_OPERATOR); + } + + + // see http://groovy.codehaus.org/Operator+Overloading + static { + Map c = compoundAssignmentToBinaryOperator; + c.put(PLUS_EQUAL,PLUS); + c.put(MINUS_EQUAL,MINUS); + c.put(MULTIPLY_EQUAL,MULTIPLY); + c.put(DIVIDE_EQUAL,DIVIDE); + c.put(INTDIV_EQUAL,INTDIV); + c.put(MOD_EQUAL,MOD); + c.put(POWER_EQUAL,POWER); + + c.put(LEFT_SHIFT_EQUAL, LEFT_SHIFT); + c.put(RIGHT_SHIFT_EQUAL, RIGHT_SHIFT); + c.put(RIGHT_SHIFT_UNSIGNED_EQUAL, RIGHT_SHIFT_UNSIGNED); + + c.put(BITWISE_OR_EQUAL, BITWISE_OR); + c.put(BITWISE_AND_EQUAL, BITWISE_AND); + c.put(BITWISE_XOR_EQUAL, BITWISE_XOR); + + // see BinaryExpressionHelper.eval + Map b = binaryOperatorMethods; + b.put(PLUS,"plus"); + b.put(MINUS,"minus"); + b.put(MULTIPLY,"multiply"); + b.put(POWER,"power"); + b.put(DIVIDE,"div"); + b.put(MOD,"mod"); + b.put(BITWISE_OR,"or"); + b.put(BITWISE_AND,"and"); + b.put(BITWISE_XOR,"xor"); + b.put(LEFT_SHIFT,"leftShift"); + b.put(RIGHT_SHIFT,"rightShift"); + b.put(RIGHT_SHIFT_UNSIGNED,"rightShiftUnsigned"); + + b.put(COMPARE_EQUAL,"compareEqual"); + b.put(COMPARE_NOT_EQUAL,"compareNotEqual"); + b.put(COMPARE_LESS_THAN,"compareLessThan"); + b.put(COMPARE_LESS_THAN_EQUAL,"compareLessThanEqual"); + b.put(COMPARE_GREATER_THAN,"compareGreaterThan"); + b.put(COMPARE_GREATER_THAN_EQUAL,"compareGreaterThanEqual"); + b.put(COMPARE_TO,"compareTo"); + + b.put(FIND_REGEX,"findRegex"); + b.put(MATCH_REGEX,"matchRegex"); + } +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/RejectEverythingInterceptor.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/RejectEverythingInterceptor.java new file mode 100644 index 000000000..218a3afd2 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/RejectEverythingInterceptor.java @@ -0,0 +1,123 @@ +/* + * The MIT License + * + * Copyright 2020 CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox.impl; + +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.kohsuke.groovy.sandbox.GroovyInterceptor; +import org.kohsuke.groovy.sandbox.GroovyInterceptor.Invoker; +/** + * An interceptor used by {@link Invoker} to reject any sandbox-transformed code that is executed when + * {@link GroovyInterceptor#getApplicableInterceptors} is empty, under the assumption that there is no legitimate + * reason to run sandbox-transformed code outside of the sandbox.

+ * + * Parameters of overridden methods with type {@link Object} are assumed to be unsafe and must be handled carefully to + * avoid security vulnerabilities. Safe operations include casting these objects to known-safe final classes such as + * {@link String}, or calling known-safe final methods such as {@link Object#getClass}. + */ +public class RejectEverythingInterceptor extends GroovyInterceptor { + + @Override + public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable { + throw new SecurityException("Rejecting unsandboxed method call: " + getClassName(receiver) + "." + method + getArgumentClassNames(args)); + } + + @Override + public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable { + throw new SecurityException("Rejecting unsandboxed static method call: " + getClassName(receiver) + "." + method + getArgumentClassNames(args)); + } + + @Override + public Object onNewInstance(Invoker invoker, Class receiver, Object... args) throws Throwable { + throw new SecurityException("Rejecting unsandboxed constructor call: " + getClassName(receiver) + getArgumentClassNames(args)); + } + + @Override + public Object onSuperCall(Invoker invoker, Class senderType, Object receiver, String method, Object... args) throws Throwable { + throw new SecurityException("Rejecting unsandboxed super method call: " + getClassName(receiver) + "." + method + getArgumentClassNames(args)); + } + + @Override + public void onSuperConstructor(Invoker invoker, Class receiver, Object... args) throws Throwable { + throw new SecurityException("Rejecting unsandboxed super constructor call: " + getClassName(receiver) + getArgumentClassNames(args)); + } + + @Override + public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable { + throw new SecurityException("Rejecting unsandboxed property get: " + getClassName(receiver) + "." + property); + } + + @Override + public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable { + throw new SecurityException("Rejecting unsandboxed property set: " + getClassName(receiver) + "." + property + " = " + getClassName(value)); + } + + @Override + public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable { + throw new SecurityException("Rejecting unsandboxed attribute get: " + getClassName(receiver) + "." + attribute); + } + + @Override + public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable { + throw new SecurityException("Rejecting unsandboxed attribute set: " + getClassName(receiver) + "." + attribute + " = " + getClassName(value)); + } + + @Override + public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable { + throw new SecurityException("Rejecting unsandboxed array get: " + getClassName(receiver) + "[" + getArrayIndex(index) + "]"); + } + + @Override + public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable { + throw new SecurityException("Rejecting unsandboxed array set: " + getClassName(receiver) + "[" + getArrayIndex(index) + "] = " + getClassName(value)); + } + + private static String getClassName(Object value) { + if (value == null) { + return null; + } else if (value instanceof Class) { + return ((Class) value).getName(); + } else { + return value.getClass().getName(); + } + } + + private static String getArgumentClassNames(Object[] args) { + return Stream.of(args) + .map(RejectEverythingInterceptor::getClassName) + .collect(Collectors.joining(", ", "(", ")")); + } + + private static String getArrayIndex(Object value) { + if (value == null) { + return "null"; + } else if (value instanceof Integer) { + return value.toString(); + } else { + return value.getClass().getName(); + } + } + +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/SandboxedMethodClosure.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/SandboxedMethodClosure.java new file mode 100644 index 000000000..5cbc99214 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/SandboxedMethodClosure.java @@ -0,0 +1,41 @@ +package org.kohsuke.groovy.sandbox.impl; + +import groovy.lang.MetaClassImpl; +import org.codehaus.groovy.runtime.InvokerInvocationException; +import org.codehaus.groovy.runtime.MethodClosure; + +import static org.codehaus.groovy.runtime.InvokerHelper.*; + +/** + * {@link MethodClosure} that checks the call. + * + * @author Kohsuke Kawaguchi + */ +public class SandboxedMethodClosure extends MethodClosure { + public SandboxedMethodClosure(Object owner, String method) { + super(owner, method); + } + + /** + * Special logic needed to handle invocation due to not being an instance of MethodClosure itself. See + * {@link MetaClassImpl#invokeMethod(Class, Object, String, Object[], boolean, boolean)} and its special handling + * of {@code objectClass == MethodClosure.class}. + */ + protected Object doCall(Object[] arguments) { + try { + return Checker.checkedCall(getOwner(), false, false, getMethod(), arguments); + } catch (Throwable e) { + throw new InvokerInvocationException(e); + } + } + + protected Object doCall() { + Object[] emptyArgs = {}; + return doCall(emptyArgs); + } + + @Override + protected Object doCall(Object arguments) { + return doCall(asArray(arguments)); + } +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/SingleArgInvokerChain.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/SingleArgInvokerChain.java new file mode 100644 index 000000000..8fb72b41a --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/SingleArgInvokerChain.java @@ -0,0 +1,32 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor; + +import java.util.Iterator; + +/** + * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. + * + * This version expects exactly one argument. + * + * @author Kohsuke Kawaguchi + */ +abstract class SingleArgInvokerChain extends InvokerChain { + protected SingleArgInvokerChain(Object receiver) { + super(receiver); + } + + public final Object call(Object receiver, String method) throws Throwable { + throw new UnsupportedOperationException(); + } + + public final Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable { + throw new UnsupportedOperationException(); + } + + public final Object call(Object receiver, String method, Object... args) throws Throwable { + if (args.length!=1) + throw new UnsupportedOperationException(); + return call(receiver,method,args[0]); + } +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Super.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Super.java new file mode 100644 index 000000000..bb6976221 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/Super.java @@ -0,0 +1,17 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor.Invoker; + +/** + * Packs argument of the super method call for {@link Invoker} + * @author Kohsuke Kawaguchi + */ +public final class Super { + final Class senderType; + final Object receiver; + + public Super(Class senderType, Object receiver) { + this.senderType = senderType; + this.receiver = receiver; + } +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/TwoArgInvokerChain.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/TwoArgInvokerChain.java new file mode 100644 index 000000000..96058787c --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/TwoArgInvokerChain.java @@ -0,0 +1,33 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor; + +import java.util.Iterator; + +/** + * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. + * + * This version expects two arguments. + * + * @author Kohsuke Kawaguchi + */ +abstract class TwoArgInvokerChain extends InvokerChain { + protected TwoArgInvokerChain(Object receiver) { + super(receiver); + } + + public final Object call(Object receiver, String method) throws Throwable { + throw new UnsupportedOperationException(); + } + + public final Object call(Object receiver, String method, Object arg1) throws Throwable { + throw new UnsupportedOperationException(); + } + + public final Object call(Object receiver, String method, Object... args) throws Throwable { + if (args.length!=2) + throw new UnsupportedOperationException(); + return call(receiver,method,args[0],args[1]); + } +} + diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/VarArgInvokerChain.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/VarArgInvokerChain.java new file mode 100644 index 000000000..19f13cd91 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/VarArgInvokerChain.java @@ -0,0 +1,30 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor; + +/** + * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. + * + * This version is optimized for arbitrary number arguments. + * + * @author Kohsuke Kawaguchi + */ +abstract class VarArgInvokerChain extends InvokerChain { + protected VarArgInvokerChain(Object receiver) { + super(receiver); + } + + public final Object call(Object receiver, String method) throws Throwable { + return call(receiver,method,EMPTY_ARRAY); + } + + public final Object call(Object receiver, String method, Object arg1) throws Throwable { + return call(receiver,method,new Object[]{arg1}); + } + + public final Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable { + return call(receiver,method,new Object[]{arg1,arg2}); + } + + private static final Object[] EMPTY_ARRAY = new Object[0]; +} diff --git a/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/ZeroArgInvokerChain.java b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/ZeroArgInvokerChain.java new file mode 100644 index 000000000..d30289d32 --- /dev/null +++ b/groovy-sandbox/src/main/java/org/kohsuke/groovy/sandbox/impl/ZeroArgInvokerChain.java @@ -0,0 +1,30 @@ +package org.kohsuke.groovy.sandbox.impl; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor; + +/** + * {@link GroovyInterceptor.Invoker} that chains multiple {@link GroovyInterceptor} instances. + * + * This version expects no arguments. + * + * @author Kohsuke Kawaguchi + */ +abstract class ZeroArgInvokerChain extends InvokerChain { + protected ZeroArgInvokerChain(Object receiver) { + super(receiver); + } + + public final Object call(Object receiver, String method, Object arg1) throws Throwable { + throw new UnsupportedOperationException(); + } + + public final Object call(Object receiver, String method, Object arg1, Object arg2) throws Throwable { + throw new UnsupportedOperationException(); + } + + public final Object call(Object receiver, String method, Object... args) throws Throwable { + if (args.length!=0) + throw new UnsupportedOperationException(); + return call(receiver,method); + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/ClassRecorder.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/ClassRecorder.java new file mode 100644 index 000000000..8de85650b --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/ClassRecorder.java @@ -0,0 +1,103 @@ +package org.kohsuke.groovy.sandbox; + +/** + * Records the interception in a short form. + * + * @author Kohsuke Kawaguchi + */ +public class ClassRecorder extends GroovyInterceptor { + private final StringBuilder buf = new StringBuilder(); + + @Override + public String toString() { + return buf.toString(); + } + + public void reset() { + buf.setLength(0); + } + + private void format(String fmt, Object... args) { + buf.append(String.format(fmt,args)).append('\n'); + } + + private String type(Object o) { + return o==null ? "null" : type(o.getClass()); + } + + private String type(Class c) { + if (c.isArray()) + return type(c.getComponentType())+"[]"; + String n = c.getName(); + return n.substring(n.lastIndexOf('.')+1); + } + + private String arguments(Object... args) { + StringBuilder b = new StringBuilder(); + for (Object o : args) { + if (b.length()>0) b.append(','); + b.append(type(o)); + } + return b.toString(); + } + + @Override + public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable { + format("%s.%s(%s)",type(receiver),method,arguments(args)); + return super.onMethodCall(invoker, receiver, method, args); + } + + @Override + public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable { + format("%s:%s(%s)",type(receiver),method,arguments(args)); + return super.onStaticCall(invoker, receiver, method, args); + } + + @Override + public Object onNewInstance(Invoker invoker, Class receiver, Object... args) throws Throwable { + format("new %s(%s)",type(receiver),arguments(args)); + return super.onNewInstance(invoker, receiver, args); + } + + @Override + public Object onSuperCall(Invoker invoker, Class senderType, Object receiver, String method, Object... args) throws Throwable { + format("%s.super(%s).%s(%s)",type(receiver),type(senderType),method,arguments(args)); + return super.onSuperCall(invoker, senderType, receiver, method, args); + } + + @Override + public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable { + format("%s.%s",type(receiver),property); + return super.onGetProperty(invoker, receiver, property); + } + + @Override + public Object onSetProperty(Invoker invoker, Object receiver, String property, Object value) throws Throwable { + format("%s.%s=%s",type(receiver),property,type(value)); + return super.onSetProperty(invoker, receiver, property, value); + } + + @Override + public Object onGetAttribute(Invoker invoker, Object receiver, String attribute) throws Throwable { + format("%s.@%s",type(receiver),attribute); + return super.onGetAttribute(invoker, receiver, attribute); + } + + @Override + public Object onSetAttribute(Invoker invoker, Object receiver, String attribute, Object value) throws Throwable { + format("%s.@%s=%s",type(receiver),attribute,type(value)); + return super.onSetAttribute(invoker, receiver, attribute, value); + } + + @Override + public Object onGetArray(Invoker invoker, Object receiver, Object index) throws Throwable { + format("%s[%s]",type(receiver),type(index)); + return super.onGetArray(invoker, receiver, index); + } + + @Override + public Object onSetArray(Invoker invoker, Object receiver, Object index, Object value) throws Throwable { + format("%s[%s]=%s",type(receiver),type(index),type(value)); + return super.onSetArray(invoker, receiver, index, value); + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/FinalizerTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/FinalizerTest.java new file mode 100644 index 000000000..3de2fd2fa --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/FinalizerTest.java @@ -0,0 +1,172 @@ +/* + * The MIT License + * + * Copyright 2018 CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox; + +import groovy.lang.GroovyShell; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.MultipleCompilationErrorsException; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.junit.Before; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; + +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +@Issue("SECURITY-1186") +public class FinalizerTest { + private static final String SCRIPT_HARNESS = + "class Global {\n" + + " static volatile boolean result = false\n" + + "}\n" + + "class Test {\n" + + " METHOD { Global.result = true; }\n" + + "}\n" + + "def t = new Test()\n" + + "t = null\n" + + // TODO: Flaky, can it be made more reliable? + "for (int i = 0; i < 10 && Global.result == false; i++) {\n" + + " System.gc()\n" + + " System.runFinalization()\n" + + " Thread.sleep(100)\n" + + "}\n" + + "Global.result"; + + private GroovyShell sandboxedSh; + private GroovyShell unsandboxedSh; + + @Before + public void setUp() { + CompilerConfiguration cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new ImportCustomizer().addImports("groovy.transform.PackageScope")); + cc.addCompilationCustomizers(new SandboxTransformer()); + sandboxedSh = new GroovyShell(cc); + cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new ImportCustomizer().addImports("groovy.transform.PackageScope")); + unsandboxedSh = new GroovyShell(cc); + } + + /** + * These scripts are forbidden by {@link SandboxTransformer#call} after the SECURITY-1186 fix. + */ + @Test + public void testOverridingFinalizeForbidden() { + assertForbidden("@Override public void finalize()", true); + assertForbidden("protected void finalize()", true); + // Groovy's default access modifier is public. + assertForbidden("void finalize()", true); + assertForbidden("def void finalize()", true); + // This finalizer would be invoked despite having @PackageScope, so it must be forbidden. + assertForbidden("@PackageScope void finalize()", true); + // Finalizers with only default parameters will cause a finalizer with no parameters to be + // introduced, so they must be forbidden. + assertForbidden("public void finalize(Object p1 = null)", true); + assertForbidden("public void finalize(Object p1 = null, Object p2 = null)", true); + assertForbidden("public void finalize(Object[] args = [null, null])", true); + assertForbidden("public void finalize(Object... args = [null, null])", true); + } + + /** + * These scripts throw compilation failures even before the fix for SECURITY-1186 because they + * are improper overrides of {@link Object#finalize}. + */ + @Test + public void testImproperOverrideOfFinalize() { + assertImproperOverride("private void finalize()"); + assertImproperOverride("private static void finalize()"); + assertImproperOverride("private Object finalize()"); + assertImproperOverride("public Object finalize()"); + assertImproperOverride("public Void finalize()"); + assertImproperOverride("def finalize()"); + } + + /** + * These classes are allowed by {@link SandboxTransformer#call} because their finalize method + * won't be invoked outside of the sandbox by the JVM. + */ + @Test + public void testFinalizePermittedAsNonOverride() { + assertFinalizerNotCalled("public static void finalize()"); + assertFinalizerNotCalled("static void finalize()"); + assertFinalizerNotCalled("protected static void finalize()"); + assertFinalizerNotCalled("public void finalize(Object p)"); + assertFinalizerNotCalled("protected void finalize(Object p)"); + assertFinalizerNotCalled("private void finalize(Object p)"); + assertFinalizerNotCalled("public void finalize(Object p1, Object p2 = null)"); + assertFinalizerNotCalled("public void finalize(Object p1 = null, Object p2)"); + assertFinalizerNotCalled("def void finalize(Map args)"); + } + + private void assertForbidden(String methodStub, boolean isDangerous) { + String script = SCRIPT_HARNESS.replace("METHOD", methodStub); + ClassRecorder cr = new ClassRecorder(); + cr.register(); + try { + sandboxedSh.evaluate(script); + fail("Should have failed"); + } catch (MultipleCompilationErrorsException e) { + assertThat(e.getErrorCollector().getErrorCount(), equalTo(1)); + Exception innerE = e.getErrorCollector().getException(0); + assertThat(innerE, instanceOf(SecurityException.class)); + assertThat(innerE.getMessage(), containsString("Object.finalize()")); + } finally { + cr.unregister(); + } + Object actual = unsandboxedSh.evaluate(script); + assertThat(actual, equalTo((Object)isDangerous)); + } + + private void assertImproperOverride(String methodStub) { + ClassRecorder cr = new ClassRecorder(); + cr.register(); + try { + sandboxedSh.evaluate(SCRIPT_HARNESS.replace("METHOD", methodStub)); + fail("Should have failed"); + } catch (MultipleCompilationErrorsException e) { + assertThat(e.getErrorCollector().getErrorCount(), equalTo(1)); + assertThat(e.getMessage(), anyOf( + containsString("cannot override finalize in java.lang.Object"), + containsString("incompatible with void in java.lang.Object"))); + } finally { + cr.unregister(); + } + } + + private void assertFinalizerNotCalled(String methodStub) { + ClassRecorder cr = new ClassRecorder(); + cr.register(); + try { + Boolean actual = (Boolean)sandboxedSh.evaluate(SCRIPT_HARNESS.replace("METHOD", methodStub)); + assertThat(actual, equalTo(false)); + } finally { + cr.unregister(); + } + } + +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/NonArrayConstructorList.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/NonArrayConstructorList.java new file mode 100644 index 000000000..6cac45b7d --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/NonArrayConstructorList.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox; + + +import java.util.ArrayList; + +/** + * Used in {@link TheTest#testCheckedCastWhenAssignable()} - couldn't be an inner class due to gmaven issues. + */ +public class NonArrayConstructorList extends ArrayList { + public NonArrayConstructorList(boolean choiceOne, boolean choiceTwo) { + if (choiceOne) { + this.add("one"); + } + if (choiceTwo) { + this.add("two"); + } + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SandboxTransformerTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SandboxTransformerTest.java new file mode 100644 index 000000000..189a3145b --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SandboxTransformerTest.java @@ -0,0 +1,1393 @@ +/* + * The MIT License + * + * Copyright 2019 CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox; + +import groovy.lang.Binding; +import groovy.lang.EmptyRange; +import groovy.lang.GroovyShell; +import groovy.lang.IntRange; +import groovy.lang.ObjectRange; +import java.io.File; +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.codehaus.groovy.runtime.ProxyGeneratorAdapter; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; +import org.jvnet.hudson.test.Issue; +import org.kohsuke.groovy.sandbox.impl.GroovyCallSiteSelector; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +public class SandboxTransformerTest { + public @Rule ErrorCollector ec = new ErrorCollector(); + public Binding binding = new Binding(); + public GroovyShell sandboxedSh; + public GroovyShell unsandboxedSh; + public ClassRecorder cr = new ClassRecorder(); + + @Before + public void setUp() { + CompilerConfiguration cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new ImportCustomizer().addImports(SandboxTransformerTest.class.getName()).addStarImports("org.kohsuke.groovy.sandbox")); + cc.addCompilationCustomizers(new SandboxTransformer()); + sandboxedSh = new GroovyShell(binding,cc); + + cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new ImportCustomizer().addImports(SandboxTransformerTest.class.getName()).addStarImports("org.kohsuke.groovy.sandbox")); + unsandboxedSh = new GroovyShell(binding,cc); + } + + public void configureBinding() { } + + /** + * Use {@code ShouldFail.class} as the expected result for {@link #sandboxedEval} and {@link #unsandboxedEval} + * when the expression is expected to throw an exception. + */ + public static final class ShouldFail { } + + @FunctionalInterface + public interface ExceptionHandler { + public void handleException(Throwable e) throws Exception; + } + + /** + * Executes a Groovy expression inside of the sandbox. + * @param expression The Groovy expression to execute. + */ + public void sandboxedEval(String expression, Object expectedResult, ExceptionHandler handler) { + cr.reset(); + cr.register(); + try { + configureBinding(); + Object actual = sandboxedSh.evaluate(expression); + String actualType = GroovyCallSiteSelector.getName(actual); + String expectedType = GroovyCallSiteSelector.getName(expectedResult); + ec.checkThat("Sandboxed result (" + actualType + ") does not match expected result (" + expectedType + ")", actual, equalTo(expectedResult)); + } catch (Throwable e) { + ec.checkSucceeds(() -> { + try { + handler.handleException(e); + } catch (Throwable t) { + t.addSuppressed(e); // Keep the original error around in case an assertion fails in the handler. + throw t; + } + return null; + }); + } finally { + cr.unregister(); + } + } + + /** + * Executes a Groovy expression outside of the sandbox. + * @param expression The Groovy expression to execute. + */ + private void unsandboxedEval(String expression, Object expectedResult, ExceptionHandler handler) { + try { + configureBinding(); + Object actual = unsandboxedSh.evaluate(expression); + String actualType = GroovyCallSiteSelector.getName(actual); + String expectedType = GroovyCallSiteSelector.getName(expectedResult); + ec.checkThat("Unsandboxed result (" + actualType + ") does not match expected result (" + expectedType + ")", actual, equalTo(expectedResult)); + } catch (Exception e) { + ec.checkSucceeds(() -> { + handler.handleException(e); + return null; + }); + } + } + + /** + * Execute a Groovy expression both in and out of the sandbox and check that the return value matches the + * expected value and that the given list of method calls are intercepted by the sandbox. + * @param expression The Groovy expression to execute. + * @param expectedReturnValue The expected return value for running the script. + * @param expectedCalls The method calls that are expected to be intercepted by the sandbox. + */ + public void assertIntercept(String expression, Object expectedReturnValue, String... expectedCalls) { + assertEvaluate(expression, expectedReturnValue); + assertIntercepted(expectedCalls); + } + + /** + * Check that the most recently executed expression intercepted the expected calls. + * Automatically adds {@code new Script(Binding)} to the list of intercepted calls. + * @param expectedCalls The method calls that were expected to be intercepted by the sandbox. + * @see #assertInterceptedExact + */ + public void assertIntercepted(String... expectedCalls) { + // Workaround to avoid having to update all existing tests. + String[] updatedExpectedCalls = expectedCalls; + if (expectedCalls.length == 0 || (expectedCalls.length > 0 && !expectedCalls[0].equals("new Script(Binding)"))) { + updatedExpectedCalls = new String[expectedCalls.length + 1]; + updatedExpectedCalls[0] = "new Script(Binding)"; + System.arraycopy(expectedCalls, 0, updatedExpectedCalls, 1, expectedCalls.length); + } + assertInterceptedExact(updatedExpectedCalls); + } + + /** + * Check that the most recently executed expression intercepted the expected calls. + * @param expectedCalls The method calls that were expected to be intercepted by the sandbox. + */ + public void assertInterceptedExact(String... expectedCalls) { + String[] interceptedCalls = cr.toString().split("\n"); + if (interceptedCalls.length == 1 && interceptedCalls[0].equals("")) { + interceptedCalls = new String[0]; + } + ec.checkThat(interceptedCalls, equalTo(expectedCalls)); + } + + /** + * Execute a Groovy expression both in and out of the sandbox and check that the return value matches the + * expected value. + * @param expression The Groovy expression to execute. + * @param expectedReturnValue The expected return value for running the script. + */ + public void assertEvaluate(String expression, Object expectedReturnValue) { + sandboxedEval(expression, expectedReturnValue, e -> { + throw new RuntimeException("Failed to evaluate sandboxed expression: " + expression, e); + }); + unsandboxedEval(expression, expectedReturnValue, e -> { + throw new RuntimeException("Failed to evaluate unsandboxed expression: " + expression, e); + }); + } + + /** + * Execute a Groovy expression both in and out of the sandbox and check that the script throws an exception with + * the same class and message in both cases. + * @param expression The Groovy expression to execute. + */ + private void assertFailsWithSameException(String expression) { + AtomicReference sandboxedException = new AtomicReference<>(); + sandboxedEval(expression, ShouldFail.class, sandboxedException::set); + AtomicReference unsandboxedException = new AtomicReference<>(); + unsandboxedEval(expression, ShouldFail.class, unsandboxedException::set); + if (sandboxedException.get() == null || unsandboxedException.get() == null) { + return; // Either sandboxedEval or unsandboxedEval will have already recorded an error because the result was not ShouldFail. + } + ec.checkThat("Sandboxed and unsandboxed exception should have the same type", + unsandboxedException.get().getClass(), equalTo(sandboxedException.get().getClass())); + ec.checkThat("Sandboxed and unsandboxed exception should have the same message", + unsandboxedException.get().getMessage(), equalTo(sandboxedException.get().getMessage())); + } + + @Issue("SECURITY-1465") + @Test public void sandboxTransformsMethodPointerLhs() throws Exception { + assertIntercept( + "({" + + " System.getProperties()\n" + + " 1" + + "}().&toString)()", + "1", + "Script1$_run_closure1.call()", + "System:getProperties()", + "SandboxedMethodClosure.call()", + "Integer.toString()"); + } + + @Issue("SECURITY-1465") + @Test public void sandboxTransformsMethodPointerRhs() throws Exception { + try { + System.setProperty("sandboxTransformsMethodPointerRhs", "toString"); + assertIntercept( + "1.&(System.getProperty('sandboxTransformsMethodPointerRhs'))()", + "1", + "System:getProperty(String)", + "SandboxedMethodClosure.call()", + "Integer.toString()"); + } finally { + System.clearProperty("sandboxTransformsMethodPointerRhs"); + } + } + + @Issue("SECURITY-1465") + @Test public void sandboxWillNotCastNonStandardCollections() throws Exception { + // Note: If you run this test in a debugger and inspect the proxied closure in Checker#preCheckedCast, the test + // will probably fail because the debugger will invoke the closure while trying to display the value, which will + // update the value of i, changing the behavior. + + /* The cast to Collection proxies the Closure to a Collection. + * The cast to File invokes `new File(... proxied collection's elements as constructor arguments)`. + * The cast to Object[] reads lines from the File. + * The trick here is that the closure returns null from the first call to `toArray` in `Checker.preCheckedCast`, + * so it bypassed the interceptor but still worked correctly in the actual cast before the fix. + */ + sandboxedEval( + "def i = 0\n" + + "(({-> if(i) {\n" + + " return ['secret.txt'] as Object[]\n" + // Cast here is just so `toArray` returns an array instead of a List. + " } else {\n" + + " i = 1\n" + + " return null\n" + + " }\n" + + "} as Collection) as File) as Object[]", + ShouldFail.class, + e -> { + assertThat(e, instanceOf(UnsupportedOperationException.class)); + assertThat(e.getMessage(), + containsString("Casting non-standard Collections to a type via constructor is not supported.")); + }); + } + + @Issue("SECURITY-1465") + @Test public void sandboxWillNotCastNonStandardCollectionsEvenIfHarmless() throws Exception { + // Not problematic even before the fix because it is consistent, but there is no good way to differentiate + // between this and the expression in sandboxWillNotCastNonStandardCollections, so they both get blocked. + sandboxedEval("(({-> return ['secret.txt'] as Object[] } as Collection) as File) as Object[]", ShouldFail.class, e -> { + assertThat(e, instanceOf(UnsupportedOperationException.class)); + assertThat(e.getMessage(), + containsString("Casting non-standard Collections to a type via constructor is not supported.")); + }); + } + + @Issue("SECURITY-1465") + @Test public void sandboxWillCastStandardCollections() throws Exception { + Path secret = Paths.get("secret.txt"); + try { + Files.write(secret, Arrays.asList("secretValue")); + assertIntercept( + "(Arrays.asList('secret.txt') as File) as Object[]", + new String[]{"secretValue"}, + "Arrays:asList(String)", + "new File(String)", + "ResourceGroovyMethods:readLines(File)"); + assertIntercept( + "(Collections.singleton('secret.txt') as File) as Object[]", + new String[]{"secretValue"}, + "Collections:singleton(String)", + "new File(String)", + "ResourceGroovyMethods:readLines(File)"); + assertIntercept( + "(new ArrayList<>(Arrays.asList('secret.txt')) as File) as Object[]", + new String[]{"secretValue"}, + "Arrays:asList(String)", + "new ArrayList(Arrays$ArrayList)", + "new File(String)", + "ResourceGroovyMethods:readLines(File)"); + assertIntercept( + "(new HashSet<>(Arrays.asList('secret.txt')) as File) as Object[]", + new String[]{"secretValue"}, + "Arrays:asList(String)", + "new HashSet(Arrays$ArrayList)", + "new File(String)", + "ResourceGroovyMethods:readLines(File)"); + } finally { + Files.deleteIfExists(secret); + } + } + + @Issue("SECURITY-1465") + @Test public void sandboxInterceptsEnumClassToArrayCasts() throws Exception { + assertIntercept( + "(java.util.concurrent.TimeUnit.class as Object[])", + TimeUnit.values(), + "Class.NANOSECONDS", + "Class.MICROSECONDS", + "Class.MILLISECONDS", + "Class.SECONDS", + "Class.MINUTES", + "Class.HOURS", + "Class.DAYS"); + } + + @Issue("SECURITY-1538") + @Test public void sandboxTransformsMethodNameInMethodCalls() throws Exception { + assertIntercept( + "1.({ System.getProperties(); 'toString' }())()", + "1", + "Script1$_run_closure1.call()", + "System:getProperties()", + "Integer.toString()"); + } + + @Issue("SECURITY-1538") + @Test public void sandboxTransformsPropertyNameInLhsOfAssignmentOps() throws Exception { + assertIntercept( + "class Test {\n" + + " def x\n" + + "}\n" + + "def t = new Test()\n" + + "t.({\n" + + " System.getProperties()\n" + + " 'x'\n" + + "}()) = 1\n" + + "t.x", + 1, + "new Test()", + "Script1$_run_closure1.call()", + "System:getProperties()", + "Test.x=Integer", + "Test.x"); + } + + @Issue("SECURITY-1538") + @Test public void sandboxTransformsPropertyNameInPrefixPostfixOps() throws Exception { + assertIntercept( + "class Test {\n" + + " def x = 0\n" + + "}\n" + + "def t = new Test()\n" + + "(t.({\n" + + " System.getProperties()\n" + + " 'x'\n" + + "}()))++\n" + + "t.x", + 1, + "new Test()", + "Script1$_run_closure1.call()", + "System:getProperties()", + "Test.x", + "Integer.next()", + "Test.x=Integer", + "Test.x"); + } + + @Issue("SECURITY-1538") + @Test public void sandboxTransformsComplexExpressionsInPrefixOps() throws Exception { + assertIntercept( + "++({ System.getProperties(); 1 }())", + 2, + "Script1$_run_closure1.call()", + "System:getProperties()", + "Integer.next()"); + } + + @Issue("SECURITY-1538") + @Test public void sandboxTransformsComplexExpressionsInPostfixOps() throws Exception { + assertIntercept( + "({ System.getProperties(); 1 }())++", + 1, + "Script1$_run_closure2.call()", + "System:getProperties()", + "Script1$_run_closure1.call(Integer)", + "Integer.next()"); + } + + @Test public void sandboxTransformsInitialExpressionsForConstructorParameters() throws Exception { + assertIntercept( + "class B { }\n" + + "class A extends B {\n" + + " A(x = System.getProperties()) {\n" + + " super()\n" + + " }\n" + + "}\n" + + "new A()\n" + + "true\n", + true, + "new A()", + "System:getProperties()", + "new A(Properties)", + "new B()"); + } + + @Issue("SECURITY-1658") + @Test public void sandboxTransformsInitialExpressionsForClosureParameters() throws Exception { + assertIntercept( + "({ p = System.getProperties() -> true })()", + true, + "Script1$_run_closure1.call()", + "System:getProperties()"); + } + + @Issue("SECURITY-1754") + @Test public void interceptThisConstructorCalls() throws Exception { + assertIntercept( + "class Superclass { }\n" + + "class Subclass extends Superclass {\n" + + " Subclass() { this(1) }\n" + + " Subclass(int x) { }\n" + + "}\n" + + "new Subclass()\n" + + "null", + null, + "new Subclass()", + "new Subclass(Integer)", + "new Superclass()"); + } + + @Issue("SECURITY-3341") + @Test public void sandboxBlocksCastingInThisConstructorCalls() throws Exception { + sandboxedEval( + "class Subclass {\n" + + " def x\n" + + " Subclass() { this(['secret.key']) }\n" + + " Subclass(File f) { this.x = f }\n" + + "}\n" + + "new Subclass().x\n", + ShouldFail.class, + e -> assertThat(e.getMessage(), containsString("Unable to find constructor: new Subclass java.util.ArrayList"))); + sandboxedEval( + "class Subclass {\n" + + " def x\n" + + " Subclass(File f) { this.x = f }\n" + + "}\n" + + "(new Subclass(['secret.key']) { def getFoo() { x } }).foo\n", + ShouldFail.class, + e -> assertThat(e.getMessage(), containsString("Unable to find constructor: new Subclass java.util.ArrayList"))); + } + + @Issue("SECURITY-3341") + @Test public void sandboxBlocksCastingInSuperConstructorCalls() throws Exception { + sandboxedEval( + "package com.cloudbees.groovy.cps\n" + + "class SerializableScript {\n" + + " def x\n" + + " SerializableScript(File f) { this.x = f }\n" + + "}\n" + + "class Subclass extends SerializableScript {\n" + + " Subclass() { super(['secret.key']) }" + + "}\n" + + "new Subclass().x\n", + ShouldFail.class, + e -> assertThat(e.getMessage(), containsString("Unable to find constructor: new com.cloudbees.groovy.cps.SerializableScript java.util.ArrayList"))); + sandboxedEval( + "package java.lang\n" + + "class Object {\n" + + " def x\n" + + " Object(File f) { this.x = f }\n" + + "}\n" + + "class Subclass extends Object {\n" + + " Subclass() { super(['secret.key']) }" + + "}\n" + + "new Subclass().x\n", + ShouldFail.class, + e -> assertThat(e.getMessage(), containsString("Prohibited package name: java.lang"))); + } + + @Issue({ "SECURITY-1754", "SECURITY-2824" }) + @Test public void blocksDirectCallsToSyntheticConstructors() throws Exception { + sandboxedEval( + "class Superclass { }\n" + + "class Subclass extends Superclass {\n" + + " Subclass() { }\n" + + "}\n" + + "new Subclass(null)\n", + ShouldFail.class, + e -> assertThat(e.getMessage(), equalTo( + "Rejecting illegal call to synthetic constructor: private Subclass(org.kohsuke.groovy.sandbox.impl.Checker$SuperConstructorWrapper). " + + "Perhaps you meant to use one of these constructors instead: public Subclass()"))); + // Calls are blocked even if you manage to obtain a valid wrapper. + sandboxedEval( + "class Superclass { Superclass(String x) { } }\n" + + "class Subclass extends Superclass {\n" + + " def wrapper\n" + + " Subclass() { super('secret.key'); def $cw = $cw; wrapper = $cw }\n" + + "}\n" + + "def wrapper = new Subclass().wrapper\n" + + "class MyFile extends File {\n" + + " MyFile(String path) {\n" + + " super(path)\n" + + " }\n" + + "}\n" + + "new MyFile(wrapper, 'unused')", + ShouldFail.class, + e -> assertThat(e.getMessage(), equalTo("Rejecting illegal call to synthetic constructor: private MyFile(org.kohsuke.groovy.sandbox.impl.Checker$SuperConstructorWrapper,java.lang.String). " + + "Perhaps you meant to use one of these constructors instead: public MyFile(java.lang.String)"))); + } + + @Issue("SECURITY-1754") + @Test public void blocksCallsToSyntheticConstructorsViaOtherConstructors() throws Exception { + sandboxedEval( + "class Superclass { }\n" + + "class Subclass extends Superclass {\n" + + " Subclass() { }\n" + + " Subclass(int x, int y) { this(null) }\n" + // Directly calls synthetic constructor generated the handle the other constructor. + "}\n" + + "new Subclass(1, 2)\n", + ShouldFail.class, + e -> assertThat(e.getMessage(), equalTo( + "Rejecting illegal call to synthetic constructor: private Subclass(org.kohsuke.groovy.sandbox.impl.Checker$SuperConstructorWrapper). " + + "Perhaps you meant to use one of these constructors instead: public Subclass(), public Subclass(int,int)"))); + } + + @Issue("SECURITY-1754") + @Test public void blocksUnintendedCallsToNonSyntheticConstructors() throws Exception { + sandboxedEval( + "class B { }\n" + + "class F extends B { }\n" + + "class S extends B {\n" + + " Object scw\n" + + " S(Object o) { }\n" + + " S(Object o, F f) { scw = o }\n" + + "}\n" + + "new S(new F()).scw", + ShouldFail.class, + e -> assertThat(e.getMessage(), equalTo( + "Rejecting unexpected invocation of constructor: public S(java.lang.Object,F). " + + "Expected to invoke synthetic constructor: private S(org.kohsuke.groovy.sandbox.impl.Checker$SuperConstructorWrapper,java.lang.Object)"))); + } + + @Issue("SECURITY-1754") + @Test public void localVarsInIfStatementsAreNotInScopeInElseStatements() throws Exception { + sandboxedEval( + "class Super { }\n" + + "class Sub extends Super {\n" + + " def var\n" + + " Sub() {\n" + + " if (false)\n" + // Intentionally not using braces for the body. + " def $cw\n" + // The name of the parameter for constructor wrappers added by `SandboxTransformer.processConstructors()`. + " else {\n" + + " this.var = $cw\n" + + " }\n" + + " }\n" + + "}\n" + + "new Sub().var\n", + ShouldFail.class, // Previously, would have been an instance of Checker.SuperConstructorWrapper. + e -> assertThat(e.getMessage(), containsString("No such property: $cw for class: Sub"))); + } + + @Issue("SECURITY-1754") + @Test public void statementsInSyntheticConstructorsAreScopedCorrectly() throws Exception { + assertIntercept( + "class Super { }\n" + + "class Sub extends Super {\n" + + " Sub() {\n" + + " def x = 1\n" + + " x += 1\n" + + " }\n" + + "}\n" + + "new Sub()\n" + + "null\n", + null, + "new Sub()", + "new Super()"); + } + + @Issue("SECURITY-2020") + @Test public void sandboxedCodeRejectedWhenExecutedOutsideOfSandbox() throws Exception { + cr.reset(); + cr.register(); + Object returnValue; + try { + returnValue = sandboxedSh.evaluate( + "class Test {\n" + + " @Override public String toString() {\n" + + " System.getProperties()\n" + + " 'test'\n" + + " }\n" + + "}\n" + + "new Test()"); + } finally { + cr.unregister(); + } + try { + // Test.equals and Test.getClass are inherited and not sandbox-transformed, so they can be called outside of the sandbox. + assertFalse(returnValue.equals(new Object())); + assertThat(returnValue.getClass().getSimpleName(), equalTo("Test")); + // Test.toString is defined in the sandbox, so it cannot be called outside of the sandbox. + returnValue.toString(); + fail("Test.toString should have thrown a SecurityException"); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("Rejecting unsandboxed static method call: java.lang.System.getProperties()")); + } + } + + @Test public void equalsAndHashCode() throws Exception { + assertIntercept( + "@groovy.transform.EqualsAndHashCode\n" + + "class C {\n" + + " def prop\n" + + " def getProp() {\n" + + " System.setProperty('x', 'y')\n" + + " 'foo'\n" + + " }\n" + + "}\n" + + "[new C().equals(new C()), new C().hashCode()]\n", + Arrays.asList(true, 105511), + "new C()", + "new C()", + "C.equals(C)", + "C.equals(null)", + "C.is(C)", + "C.canEqual(C)", + "C.getProp()", + "System:setProperty(String,String)", + "C.getProp()", + "System:setProperty(String,String)", + "String.compareTo(String)", + "new C()", + "C.hashCode()", + "HashCodeHelper:initHash()", + // `getProp` is called twice by the generated `hashCode` method. The first call is used to prevent cycles in case it returns `this`. + "C.getProp()", + "System:setProperty(String,String)", + "String.is(C)", + "C.getProp()", + "System:setProperty(String,String)", + "HashCodeHelper:updateHash(Integer,String)"); + } + + @Test public void sandboxInterceptsUnaryOperatorExpressions() { + assertIntercept( + "def auditLog = []\n" + + "def o = new SandboxTransformerTest.OperatorOverloader(auditLog, 2)\n" + + "[-o, +o, ~o, *auditLog]", + Arrays.asList(-2, 2, ~2, "negative", "positive", "bitwiseNegate"), + "new SandboxTransformerTest$OperatorOverloader(ArrayList,Integer)", + "SandboxTransformerTest$OperatorOverloader.negative()", + "SandboxTransformerTest$OperatorOverloader.positive()", + "SandboxTransformerTest$OperatorOverloader.bitwiseNegate()"); + } + + @Test public void sandboxInterceptsRangeExpressions() { + assertIntercept( + "def auditLog = []\n" + + "def range = new SandboxTransformerTest.OperatorOverloader(auditLog, 1)..<(new SandboxTransformerTest.OperatorOverloader(auditLog, 4))\n" + + "def result = []\n" + + "for (o in range) { result.add(o.value) }\n" + + "result.addAll(auditLog)\n" + + "result\n", + // These are the calls that actually happened at runtime. + Arrays.asList(1, 2, 3, "compareTo", "compareTo", "previous", "compareTo", "compareTo", "next", "compareTo", "compareTo", "next", "compareTo", "compareTo", "next", "compareTo", "compareTo", "next", "next"), + "new SandboxTransformerTest$OperatorOverloader(ArrayList,Integer)", + "new SandboxTransformerTest$OperatorOverloader(ArrayList,Integer)", + // These next 10 interceptions are from Checker.checkedRange and Checker.checkedComparison. + "SandboxTransformerTest$OperatorOverloader.compareTo(SandboxTransformerTest$OperatorOverloader)", + "SandboxTransformerTest$OperatorOverloader.compareTo(SandboxTransformerTest$OperatorOverloader)", + "SandboxTransformerTest$OperatorOverloader.previous()", + "SandboxTransformerTest$OperatorOverloader.compareTo(null)", + "SandboxTransformerTest$OperatorOverloader.next()", + "SandboxTransformerTest$OperatorOverloader.previous()", + "SandboxTransformerTest$OperatorOverloader.compareTo(null)", + "SandboxTransformerTest$OperatorOverloader.next()", + "SandboxTransformerTest$OperatorOverloader.previous()", + "SandboxTransformerTest$OperatorOverloader.value", + "ArrayList.add(Integer)", + "SandboxTransformerTest$OperatorOverloader.value", + "ArrayList.add(Integer)", + "SandboxTransformerTest$OperatorOverloader.value", + "ArrayList.add(Integer)", + "ArrayList.addAll(ArrayList)"); + } + + @Test public void unaryExpressionsSmoke() { + // Bitwise negate + assertEvaluate("~1", ~1); + assertEvaluate("~2L", ~2L); + assertEvaluate("~BigInteger.valueOf(3L)", BigInteger.valueOf(3L).not()); + assertEvaluate("(~'test').matcher('test').matches()", true); // Pattern does not override equals or hashcode. + assertEvaluate("(~\"tes${'t'}\").matcher('test').matches()", true); // Pattern does not override equals or hashcode. + assertEvaluate("~[1, 2L]", Arrays.asList(~1, ~2L)); + // Unary minus + assertEvaluate("-1", -1); + assertEvaluate("-2L", -2L); + assertEvaluate("-BigInteger.valueOf(3L)", BigInteger.valueOf(3L).negate()); + assertEvaluate("-4.1", BigDecimal.valueOf(4.1).negate()); + assertEvaluate("-5.2d", -5.2); + assertEvaluate("-6.3f", -6.3f); + assertEvaluate("-(short)7", (short)(-7)); + assertEvaluate("-(byte)8", (byte)(-8)); + assertEvaluate("-[1, 2L, 6.3f]", Arrays.asList(-1, -2L, -6.3f)); + // Unary plus + assertEvaluate("+1", 1); + assertEvaluate("+2L", 2L); + assertEvaluate("+BigInteger.valueOf(3L)", BigInteger.valueOf(3L)); + assertEvaluate("+4.1", BigDecimal.valueOf(4.1)); + assertEvaluate("+5.2d", 5.2); + assertEvaluate("+6.3f", 6.3f); + assertEvaluate("+(short)7", (short)7); + assertEvaluate("+(byte)8", (byte)8); + assertEvaluate("+[1, 2L, 6.3f]", Arrays.asList(1, 2L, 6.3f)); + } + + @Test + public void rangeExpressionsSmoke() { + assertEvaluate("1..3", new IntRange(true, 1, 3)); + assertEvaluate("1..<3", new IntRange(false, 1, 3)); + assertEvaluate("'a'..'c'", new ObjectRange('a', 'c')); + assertEvaluate("'a'..<'c'", new ObjectRange('a', 'b')); + assertEvaluate("'a'..<'a'", new EmptyRange('a')); + assertEvaluate("1..<1", new EmptyRange(1)); + assertEvaluate("'A'..67", new IntRange(true, 65, 67)); + assertEvaluate("'a'..'ab'", new ObjectRange("a", "ab")); + assertEvaluate("'ab'..'a'", new ObjectRange("ab", "a")); + // Checking consistency in error messages. + assertFailsWithSameException("'a'..67"); + assertFailsWithSameException("null..1"); + assertFailsWithSameException("1..null"); + assertFailsWithSameException("null..null"); + assertFailsWithSameException("1..'abc'"); + assertFailsWithSameException("'abc'..1"); + assertFailsWithSameException("(new Object())..1"); + assertFailsWithSameException("1..(new Object())"); + } + + private static class OperatorOverloader implements Comparable { + private final List auditLog; + private final int value; + + private OperatorOverloader(List auditLog, int value) { + this.auditLog = auditLog; + this.value = value; + } + + public int negative() { + auditLog.add("negative"); + return -value; + } + + public int positive() { + auditLog.add("positive"); + return value; + } + + public int bitwiseNegate() { + auditLog.add("bitwiseNegate"); + return ~value; + } + + @Override + public int compareTo(OperatorOverloader other) { + auditLog.add("compareTo"); + return Integer.compare(value, other.value); + } + + public OperatorOverloader next() { + auditLog.add("next"); + return new OperatorOverloader(auditLog, value + 1); + } + + public OperatorOverloader previous() { + auditLog.add("previous"); + return new OperatorOverloader(auditLog, value - 1); + } + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsMethodReturnValues() { + assertIntercept( + "File createFile(String path) {\n" + + " [path]\n" + + "}\n" + + "createFile('secret.key')\n", + new File("secret.key"), + "Script1.createFile(String)", + "new File(String)"); + assertIntercept( + "File createFile(String path) {\n" + + " return [path]\n" + + "}\n" + + "createFile('secret.key')\n", + new File("secret.key"), + "Script2.createFile(String)", + "new File(String)"); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsVariableAssignment() { + assertIntercept( + "File file\n" + + "file = ['secret.key']\n " + + "file", + new File("secret.key"), + "new File(String)"); + } + + // https://github.com/jenkinsci/groovy-sandbox/issues/7 would allow these casts to be intercepted here, but for now, + // we handle them in script-security's SandboxInterceptor + @Ignore("These casts cannot be intercepted by groovy-sandbox itself without extensive modifications") + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsPropertyAndAttributeAssignment() { + assertIntercept( + "class Test {\n" + + " File file\n" + + "}\n" + + "def t = new Test()\n" + + "t.file = ['secret1.key']\n " + + "def temp = t.file\n" + + "t.@file = ['secret2.key']\n " + + "[temp, t.@file]\n", + Arrays.asList(new File("secret1.key"), new File("secret2.key")), + "new Test()", + "new File(String)", + "Test.file=File", + "Test.file", + "new File(String)", + "Test.@file=File", + "Test.@file"); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsPropertyAssignmentThisField() { + assertIntercept( + "class Test {\n" + + " File file\n" + + " def setFile(String path) {\n" + + " this.file = [path]\n" + // This form of property access is handled as a special case in SandboxTransformer + " }\n" + + "}\n" + + "def t = new Test()\n" + + "t.setFile('secret.key')\n " + + "t.file", + new File("secret.key"), + "new Test()", + "Test.setFile(String)", + "new File(String)", + "Test.file"); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsArrayAssignment() { + // Regular Groovy casts the rhs of array assignments to match the component type of the array, but the + // sandbox does not do this. Ideally the sandbox would have the same behavior as regular Groovy, but the + // current behavior is safe, which is good enough. + sandboxedEval( + "File[] files = [null]\n" + + "files[0] = ['secret.key']\n" + + "files[0]", + ShouldFail.class, + e -> ec.checkThat(e.toString(), equalTo("java.lang.ArrayStoreException: java.util.ArrayList"))); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsInitialParameterExpressions() { + assertIntercept( + "def method(File file = ['secret.key']) { file }; method()", + new File("secret.key"), + "Script1.method()", + "new File(String)", + "Script1.method(File)"); + assertIntercept( + "({ File file = ['secret.key'] -> file })()", + new File("secret.key"), + "Script2$_run_closure1.call()", + "new File(String)"); + assertIntercept( + "class Test {\n" + + " def x\n" + + " Test(File file = ['secret.key']) {\n" + + " x = file\n" + + " }\n" + + "}\n" + + "new Test().x", + new File("secret.key"), + "new Test()", + "new File(String)", + "new Test(File)", + "Test.x"); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsImplicitCastsFields() { + assertIntercept( + "class Test {\n" + + " File file = ['secret.key']\n" + + "}\n" + + "new Test().file", + new File("secret.key"), + "new Test()", + "new File(String)", + "Test.file"); + assertIntercept( + "@groovy.transform.Field File file = ['secret.key']\n" + + "file", + new File("secret.key"), + "new File(String)", + "Script2.file"); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxInterceptsElementCastsInArrayCasts() { + assertIntercept( + "([['secret.key']] as File[])[0]", + new File("secret.key"), + "new File(String)", + "File[][Integer]"); + assertIntercept( + "(([['secret.key']] as Object[]) as File[])[0]", + new File("secret.key"), + "new File(String)", + "File[][Integer]"); + assertIntercept( + "((File[])[['secret.key']])[0]", + new File("secret.key"), + "new File(String)", + "File[][Integer]"); + assertIntercept( + "((File[])((Object[])[['secret.key']]))[0]", + new File("secret.key"), + "new File(String)", + "File[][Integer]"); + assertIntercept( + "([[['secret.key']]] as File[][])[0][0]", + new File("secret.key"), + "new File(String)", + "File[][][Integer]", + "File[][Integer]"); + } + + @Issue("SECURITY-3792") + @Test + public void sandboxInterceptsImplicitCastsInForEachLoops() { + // A typed for-each whose element is a Collection triggers an implicit per-element cast to the + // loop type; castToType invokes a matching constructor (e.g. ['secret.key'] -> new File(String)). + // The for-each rewrite routes that cast through the sandbox so the constructor is intercepted. + assertIntercept( + "for (File f in [['secret.key']]) { return f }", + new File("secret.key"), + "new File(String)"); + } + + @Issue("SECURITY-3792") + @Test + public void sandboxInterceptsImplicitCastsInForEachLoopsWithArrayElement() { + // A typed for-each whose element is an Object[] also triggers an implicit per-element cast to + // the loop type. The for-each rewrite only routes that cast through the sandbox; the refusal + // itself is pre-existing Checker behavior (it has never supported array-to-type constructor + // coercion), so once the cast reaches preCheckedCast it fails fast with + // UnsupportedOperationException rather than ever invoking new File(String). + sandboxedEval( + "for (File f in [['secret.key'] as Object[]]) { return f }", + ShouldFail.class, + e -> { + assertThat(e, instanceOf(UnsupportedOperationException.class)); + assertThat(e.getMessage(), + containsString("casting arrays to types via constructor is not yet supported")); + }); + } + + @Issue("SECURITY-3792") + @Test + public void sandboxDoesNotAffectLegitimateForEachLoops() { + // A typed loop whose elements already have the loop variable's type still iterates normally: + // the rewrite injects a checked cast, but casting an element to a type it already has is a + // no-op that intercepts nothing. + assertIntercept( + "def out = []\n" + + "for (String s in ['a', 'b']) { out.add(s) }\n" + + "out", + Arrays.asList("a", "b"), + "ArrayList.add(String)", + "ArrayList.add(String)"); + // Java-style for loops use a dummy variable (not a real declaration) and must be left untouched. + assertEvaluate( + "int total = 0\n" + + "for (int j = 0; j < 3; j++) { total += j }\n" + + "total", + 3); + } + + @Issue("SECURITY-3792") + @Test + public void sandboxPreservesForEachLoopBodyLabels() { + // The rewrite wraps the original loop body in a new block (to prepend the checked cast); it + // nests the body rather than flattening it so a label on the body block, and break/continue + // targeting that label, keep working. Using a File loop ensures the rewrite actually fires + // (the intercepted new File(String) proves it), and break lbl on the first element stops + // after one iteration. + assertIntercept( + "def names = []\n" + + "for (File f in [['a'], ['b']]) lbl: { names.add(f.name); break lbl }\n" + + "names", + Arrays.asList("a"), + "new File(String)", + "File.name", + "ArrayList.add(String)"); + } + + @Issue("SECURITY-2824") + @Test + public void sandboxUsesCastToTypeForImplicitCasts() { + assertIntercept( + "class Test {\n" + + " def auditLog = []\n" + + " def asType(Class c) {\n" + + " auditLog.add('asType')\n" + + " 'Test.asType'\n" + + " }\n" + + " String toString() {\n" + + " auditLog.add('toString')\n" + + " 'Test.toString'\n" + + " }\n" + + "}\n" + + "def t = new Test()\n" + + "String methodReturnValue(def o) { o }\n" + + "methodReturnValue(t)\n" + + "String variable = t\n" + + "String[] array = [t]\n" + + "(String)t\n" + + "t as String\n" + // This is the only cast that should call asType. + "t.auditLog\n", + Arrays.asList("toString", "toString", "toString", "toString", "asType"), + "new Test()", + "Script1.methodReturnValue(Test)", + "Test.auditLog", + "ArrayList.add(String)", + "Test.auditLog", + "ArrayList.add(String)", + "Test.auditLog", + "ArrayList.add(String)", + "Test.auditLog", + "ArrayList.add(String)", + "Test.auditLog", + "ArrayList.add(String)", + "Test.auditLog"); + } + + @Test + public void sandboxInterceptsAttributeExpressionsInPrefixPostfixOps() { + assertIntercept( + "class Test { int x }\n" + + "def t = new Test()\n" + + "t.@x++\n" + + "t.@x\n", + 1, + "new Test()", "Test.@x", "Integer.next()", "Test.@x=Integer", "Test.@x"); + } + + @Test + public void sandboxInterceptsEnums() { + assertIntercept( + "enum Test { FIRST, SECOND }\n" + + "Test.FIRST.toString()\n", + "FIRST", + // Enum classes are generated before SandboxTransformer runs, so various synthetic constructs are + // (unnecessarily?) intercepted if you do not define an explicit constructor. + "Class.FIRST", + "Test:$INIT(String,Integer)", + "new LinkedHashMap()", + "new Test(String,Integer,LinkedHashMap)", + "new Enum(String,Integer)", + "LinkedHashMap.equals(null)", + "ImmutableASTTransformation:checkPropNames(Test,LinkedHashMap)", + "Test:$INIT(String,Integer)", + "new LinkedHashMap()", + "new Test(String,Integer,LinkedHashMap)", + "new Enum(String,Integer)", + "LinkedHashMap.equals(null)", + "ImmutableASTTransformation:checkPropNames(Test,LinkedHashMap)", + "Class.@FIRST", + "Class.@SECOND", + "Class.@FIRST", + "Class.@SECOND", + "Test.toString()"); + assertIntercept( + "enum Test { FIRST(), SECOND(); Test() {} }\n" + + "Test.FIRST.toString()\n", + "FIRST", + // You can define an explicit constructor to simplify the generated code. + "Class.FIRST", + "Test:$INIT(String,Integer)", + "new Enum(String,Integer)", + "Test:$INIT(String,Integer)", + "new Enum(String,Integer)", + "Class.@FIRST", + "Class.@SECOND", + "Class.@FIRST", + "Class.@SECOND", + "Test.toString()"); + } + + @Test + public void sandboxInterceptsBooleanCasts() { + assertIntercept("null as Boolean", null); + assertIntercept("true as Boolean", true); + assertIntercept("[:] as Boolean", false, + "LinkedHashMap.asBoolean()"); + assertIntercept("[] as Boolean", false, + "ArrayList.asBoolean()"); + assertIntercept("[false] as Boolean", true, + "ArrayList.asBoolean()"); + assertIntercept("new Object() as Boolean", true, + "new Object()", + "Object.asBoolean()"); + assertIntercept("new Object() { boolean asBoolean() { false } } as Boolean", false, + "new Script7$1(Script7)", + "Script7$1.@this$0=Script7", + "Script7$1.asBoolean()"); + } + + @Test + public void sandboxAllowsBoxedPrimitiveCasts() { + assertIntercept("1.0 as Integer", 1); + assertFailsWithSameException("[] as Integer"); + assertIntercepted(); + assertFailsWithSameException("[] as int"); + assertIntercepted(); + assertIntercept("1 as Double", 1.0); + assertFailsWithSameException("[] as Double"); + assertIntercepted(); + assertFailsWithSameException("[] as double"); + assertIntercepted(); + assertIntercept("'test' as Character", 't'); + assertFailsWithSameException("[] as Character"); + assertIntercepted(); + assertFailsWithSameException("[] as char"); + assertIntercepted(); + assertIntercept("1 as String", "1"); + assertIntercept("[] as String", "[]"); + } + + @Test + public void sandboxInterceptsCastsToAbstractClasses() throws Throwable { + // Other tests that generate proxy classes will increment the counter. + // TODO: Could flake if tests are configured to run in parallel in the same JVM. + Field pxyCounterField = ProxyGeneratorAdapter.class.getDeclaredField("pxyCounter"); + pxyCounterField.setAccessible(true); + AtomicLong pxyCounter = (AtomicLong) pxyCounterField.get(null); + long counter = pxyCounter.get() + 1; + assertIntercept( + "def proxy = { -> 'overridden' } as org.kohsuke.groovy.sandbox.SandboxTransformerTest.AbstractClass\n" + + "[proxy.get(), proxy.get2()]", + Arrays.asList("overridden", "overridden"), + "new SandboxTransformerTest$AbstractClass()", + "SandboxTransformerTest$AbstractClass" + counter + "_groovyProxy.get()", + "SandboxTransformerTest$AbstractClass" + counter + "_groovyProxy.get2()"); + counter = pxyCounter.get() + 1; + assertIntercept( + "def proxy = ['get': { -> 'overridden' }] as org.kohsuke.groovy.sandbox.SandboxTransformerTest.AbstractClass\n" + + "[proxy.get(), proxy.get2()]", + Arrays.asList("overridden", "default"), + "new SandboxTransformerTest$AbstractClass()", + "SandboxTransformerTest$AbstractClass" + counter + "_groovyProxy.get()", + "SandboxTransformerTest$AbstractClass" + counter + "_groovyProxy.get2()"); + } + + public static abstract class AbstractClass { + public abstract Object get(); + public Object get2() { + return "default"; + } + public abstract void thisMethodExistsToAvoidCodePathsForSingleAbstractMethodClasses(); + } + + @Test + public void sandboxDoesNotMutateReturnStatementConstant() { + sandboxedEval("", null, null); + sandboxedEval("", null, null); + unsandboxedEval("", null, ec::addError); + } + + @Issue("JENKINS-69899") + @Test + public void sandboxDoesNotCastEmptyExpression() { + assertIntercept("@groovy.transform.Field String x", null); + assertIntercept("@groovy.transform.Field String x; x = null", null); + } + + @Test + public void sandboxSupportsConstructorsWithVarArgs() throws Exception { + assertIntercept( + "def result = []\n" + + "class Test {\n" + + " Test(List result, Integer... vals) {\n" + + " result.add(vals.sum())\n" + + " }\n" + + "}\n" + + "new Test(result, 1)\n" + + "new Test(result, 2, 3)\n" + + "new Test(result)\n" + + "result", + Arrays.asList(1, 5, null), + "new Test(ArrayList,Integer)", + "Integer[].sum()", + "ArrayList.add(Integer)", + "new Test(ArrayList,Integer,Integer)", + "Integer[].sum()", + "ArrayList.add(Integer)", + "new Test(ArrayList)", + "Integer[].sum()", + "ArrayList.add(null)"); + } + + @Test + public void sandboxSupportsReturnStatementsInClosures() throws Exception { + assertIntercept( + "@groovy.transform.Field\n" + + "private static final field = [\n" + + " key: { x -> return x == 123 }\n" + + "]\n" + + "field.key(123)\n", + true, + "Script1.field", + "LinkedHashMap.key(Integer)", + "Integer.compareTo(Integer)"); + assertIntercept( + "File method() {\n" + + " def field = [\n" + + " key: { x -> return x }\n" + + " ]\n" + + " result = field.key(['secret.key'])\n" + + " null\n" + + "}\n" + + "method()\n" + + "result", + Arrays.asList("secret.key"), + "Script2.method()", + "LinkedHashMap.key(ArrayList)", + "Script2.result=ArrayList", + "Script2.result"); + } + + @Test public void closureVariablesInLoopExpressions() throws Exception { + assertIntercept( + "for (int x = 0; ({s -> s})(true); x++) {\n" + + " return true\n" + + "}\n" + + "return false\n", + true, + "Script1$_run_closure1.call(Boolean)"); + assertIntercept( + "while (({s -> s})(true)) {\n" + + " return true\n" + + "}\n" + + "return false\n", + true, + "Script2$_run_closure1.call(Boolean)"); + assertIntercept( + "while (({it})(true)) {\n" + + " return true\n" + + "}\n" + + "return false\n", + true, + "Script3$_run_closure1.call(Boolean)"); + } + + @Test public void forLoopDummyParameterIsNotDeclared() { + assertFailsWithSameException( + "for (int i = 0; i < 1; i++) {\n" + + " println(forLoopDummyParameter)\n" + + "}\n"); + } + + @Test + public void sandboxSupportsFinalFields() { + assertIntercept( + "class Test {\n" + + " final String p\n" + + " Test() {\n" + + " p = 'value'\n" + + " }\n" + + "}\n" + + "new Test().p", + "value", + // Intercepted operations: + "new Test()", + "Test.p"); + } + + @Test + public void sandboxDoesNotRecurseInfinitelyInSetters() { + assertIntercept( + "class Test {\n" + + " String prop\n" + + " def setProp(newProp) {\n" + + " prop = newProp\n" + + " prop\n" + + " }\n" + + "}\n" + + "new Test().prop = 'value'", + "value", + // Intercepted operations: + "new Test()", + "Test.prop=String", + "Test.prop"); + } + + @Test + public void sandboxDoesNotPerformImplicitCastsForOverloadedOperators() { + // groovy.lang.MissingMethodException: No signature of method: OverridePlus.plus() is applicable for argument types: (java.util.ArrayList) values: [[secret.key]] + assertFailsWithSameException( + "class OverridePlus {\n" + + " def file\n" + + " OverridePlus plus(File file) {\n" + + " this.file = file\n" + + " this\n" + + " }\n" + + "}\n" + + "new OverridePlus() + ['secret.key']\n"); + } + + @Issue("JENKINS-70080") + @Test + public void sandboxSupportsCompoundAssignmentsToFields() throws Throwable { + assertIntercept( + "class Test {\n" + + " def map = [:]\n" + + " def add(newMap) {\n" + + " map += newMap\n" + + " map\n" + + " }\n" + + "}\n" + + "new Test().add([k: 'v'])\n", + Collections.singletonMap("k", "v"), + // Intercepted operations: + "new Test()", + "Test.add(LinkedHashMap)", + "LinkedHashMap.plus(LinkedHashMap)", + "Test.map"); + assertIntercept( + "class Test {\n" + + " final Map map = [:]\n" + + " Test(newMap) {\n" + + " map += newMap\n" + // Groovy is more lenient with 'final' than Java + " }\n" + + "}\n" + + "new Test([k: 'v']).map\n", + Collections.singletonMap("k", "v"), + // Intercepted operations: + "new Test(LinkedHashMap)", + "LinkedHashMap.plus(LinkedHashMap)", + "Test.map"); + assertFailsWithSameException( + "class Test {\n" + + " final Map map\n" + + " Test(newMap) {\n" + + " map += newMap\n" + // java.lang.NullPointerException: Cannot execute null+{} + " }\n" + + "}\n" + + "new Test([:]).map\n"); + } + +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SimpleNamedBean.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SimpleNamedBean.java new file mode 100644 index 000000000..9f8a43f32 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SimpleNamedBean.java @@ -0,0 +1,37 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox; + +public class SimpleNamedBean { + private String name; + + public SimpleNamedBean(String n) { + this.name = n; + } + + public String getName() { + return name; + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SomeBean.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SomeBean.java new file mode 100644 index 000000000..74ed46ea0 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/SomeBean.java @@ -0,0 +1,25 @@ +package org.kohsuke.groovy.sandbox; + +/** + * For testing field and attribute access. + * + * @author Kohsuke Kawaguchi + */ +public class SomeBean { + private int x; + + public SomeBean(int x, int y) { + this.x = x; + this.y = y; + } + + int getX() { + return x; + } + + void setX(int x) { + this.x = x; + } + + public int y; +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/StaticMethodSelectionTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/StaticMethodSelectionTest.java new file mode 100644 index 000000000..054041cb4 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/StaticMethodSelectionTest.java @@ -0,0 +1,71 @@ +package org.kohsuke.groovy.sandbox; + +import groovy.lang.GroovyShell; +import static org.junit.Assert.fail; +import org.junit.Test; + +/** + * + * + * @author Kohsuke Kawaguchi + */ +public class StaticMethodSelectionTest { + + public static void strangeThirdSelection(Class x, Class y) { + fail("I'm expecting this method not to be invoked"); + } + + /* + A part of call routing to onMethodCall vs onStaticCall requires that we emulate the groovy's method + picking logic. This is implemented inside Groovy in MetaClassImpl.chooseMethod. + + In 1.8.5, The first call to chooseMethod picks a static method defined on the class, + then the 2nd check looks for instance methods from java.lang.Class. + + But the third one is strange, as it's checking the static methods defined on this class again, + but with extra MetaClassHelper.convertToTypeArray(arguments)). Since arguments is already Class[], + this means it will find a method like static void Foo.foo(Class,Class) against a call + like Foo.foo(1,2). When I tried this in a test, the call subsequently fail with + java.lang.reflect.Method.invoke(): + + This is most likely a bug in Groovy, but since I cannot be certain, writing a test case here + to monitor the behaviour change. + + private MetaMethod pickStaticMethod(String methodName, Class[] arguments) { + MetaMethod method = null; + MethodSelectionException mse = null; + Object methods = getStaticMethods(theClass, methodName); + + if (!(methods instanceof FastArray) || !((FastArray)methods).isEmpty()) { + try { + method = (MetaMethod) chooseMethod(methodName, methods, arguments); + } catch(MethodSelectionException msex) { + mse = msex; + } + } + if (method == null && theClass != Class.class) { + MetaClass classMetaClass = registry.getMetaClass(Class.class); + method = classMetaClass.pickMethod(methodName, arguments); + } + if (method == null) { + method = (MetaMethod) chooseMethod(methodName, methods, MetaClassHelper.convertToTypeArray(arguments)); + } + + if (method == null && mse != null) { + throw mse; + } else { + return method; + } + } + + */ + @Test + public void testStrangeThirdSelection() { + try { + new GroovyShell().evaluate("org.kohsuke.groovy.sandbox.StaticMethodSelectionTest.strangeThirdSelection(1, 2)"); + fail(); + } catch (IllegalArgumentException e) { + assert e.getMessage().contains("argument type mismatch"); + } + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/TheTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/TheTest.java new file mode 100644 index 000000000..df8948f63 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/TheTest.java @@ -0,0 +1,835 @@ +package org.kohsuke.groovy.sandbox; + +import org.codehaus.groovy.runtime.NullObject; +import org.codehaus.groovy.runtime.ProxyGeneratorAdapter; +import org.jvnet.hudson.test.Issue; +import java.awt.Point; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicLong; +import org.codehaus.groovy.runtime.ResourceGroovyMethods; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; + +/** + * + * + * @author Kohsuke Kawaguchi + */ +public class TheTest extends SandboxTransformerTest { + @Override + public void configureBinding() { + binding.setProperty("foo", "FOO"); + binding.setProperty("bar", "BAR"); + binding.setProperty("zot", 5); + binding.setProperty("point", new Point(1, 2)); + binding.setProperty("points", Arrays.asList(new Point(1, 2), new Point(3, 4))); + binding.setProperty("intArray", new int[] { 0, 1, 2, 3, 4 }); + } + + private void assertIntercept(String expectedCallSequence, Object expectedValue, String script) throws Exception { + String[] expectedCalls = expectedCallSequence.isEmpty() ? new String[0] : expectedCallSequence.split("/"); + assertIntercept(script, expectedValue, expectedCalls); + } + + private void assertInterceptNoScript(String expectedCallSequence, Object expectedValue, String script) throws Exception { + String[] expectedCalls = expectedCallSequence.isEmpty() ? new String[0] : expectedCallSequence.split("/"); + assertEvaluate(script, expectedValue); + assertInterceptedExact(expectedCalls); + } + + private void assertIntercept(List expectedCallSequence, Object expectedValue, String script) throws Exception { + assertIntercept(script, expectedValue, expectedCallSequence.toArray(new String[0])); + } + + @Test public void testOK() throws Exception { + // instance call + assertIntercept( + "Integer.class/Class:forName(String)", + String.class, + "5.class.forName('java.lang.String')"); + + assertIntercept( + "String.toString()/String.hashCode()", + "foo".hashCode(), + "'foo'.toString().hashCode()" + ); + + // static call + assertIntercept(// turns out this doesn't actually result in onStaticCall + "Math:max(Float,Float)", + Math.max(1f,2f), + "Math.max(1f,2f)" + ); + + assertIntercept(// ... but this does + "Math:max(Float,Float)", + Math.max(1f,2f), + "import static java.lang.Math.*; max(1f,2f)" + ); + + // property access + assertIntercept( + "String.class/Class.name", + String.class.getName(), + "'foo'.class.name" + ); + + // constructor & field access + assertIntercept( + "new Point(Integer,Integer)/Point.@x", + 1, + "new java.awt.Point(1,2).@x" + ); + + // property set + assertIntercept( + "Script7.point/Point.x=Integer", + 3, + "point.x=3" + ); + assertEquals(3, ((Point)binding.getProperty("point")).x); + + // attribute set + assertIntercept( + "Script8.point/Point.@x=Integer", + 4, + "point.@x=4" + ); + assertEquals(4, ((Point)binding.getProperty("point")).x); + + // property spread + assertIntercept( + "Script9.points/Point.x=Integer/Point.x=Integer", + 3, + "points*.x=3" + ); + assertEquals(3, ((List)binding.getProperty("points")).get(0).x); + assertEquals(3, ((List)binding.getProperty("points")).get(1).x); + + // array set & get + assertIntercept( + "int[][Integer]=Integer/int[][Integer]", + 1, + "def x=new int[3];x[0]=1;x[0]" + ); + } + + @Test public void testClosure() throws Exception { + assertIntercept( + "Script1$_run_closure1.call()/Integer.class/Class:forName(String)", + null, + "def foo = { 5.class.forName('java.lang.String') }\n" + + "foo()\n" + + "return null"); + } + + @Test public void testClass() throws Exception { + assertInterceptNoScript( + "Integer.class/Class:forName(String)", + null, + "class foo { static void main(String[] args) throws Exception { 5.class.forName('java.lang.String') } }"); + } + + @Test public void testInnerClass() throws Exception { + assertInterceptNoScript( + "foo$bar:juu()/Integer.class/Class:forName(String)", + null, + "class foo {\n" + + " class bar {\n" + + " static void juu() throws Exception { 5.class.forName('java.lang.String') }\n" + + " }\n" + + "static void main(String[] args) throws Exception { bar.juu() }\n" + + "}"); + } + + @Test public void testStaticInitializationBlock() throws Exception { + assertInterceptNoScript( + "Integer.class/Class:forName(String)", + null, + "class foo {\n" + + "static { 5.class.forName('java.lang.String') }\n" + + " static void main(String[] args) throws Exception { }\n" + + "}"); + } + + @Test public void testConstructor() throws Exception { + assertIntercept( + "new foo()/Integer.class/Class:forName(String)", + null, + "class foo {\n" + + "foo() { 5.class.forName('java.lang.String') }\n" + + "}\n" + + "new foo()\n" + + "return null"); + } + + @Test public void testInitializationBlock() throws Exception { + assertIntercept( + "new foo()/Integer.class/Class:forName(String)", + null, + "class foo {\n" + + "{ 5.class.forName('java.lang.String') }\n" + + "}\n" + + "new foo()\n" + + "return null"); + } + + @Test public void testFieldInitialization() throws Exception { + assertIntercept( + "new foo()/Integer.class/Class:forName(String)", + null, + "class foo {\n" + + "def obj = 5.class.forName('java.lang.String')\n" + + "}\n" + + "new foo()\n" + + "return null"); + } + + @Test public void testStaticFieldInitialization() throws Exception { + assertIntercept( + "new foo()/Integer.class/Class:forName(String)", + null, + "class foo {\n" + + "static obj = 5.class.forName('java.lang.String')\n" + + "}\n" + + "new foo()\n" + + "return null"); + } + + @Test public void testCompoundAssignment() throws Exception { + assertIntercept( + "Script1.point/Point.x/Double.plus(Integer)/Point.x=Double", + (double)4.0, + "point.x += 3"); + } + + @Test public void testCompoundAssignment2() throws Exception { + // "[I" is the type name of int[] + assertIntercept( + "Script1.intArray/int[][Integer]/Integer.leftShift(Integer)/int[][Integer]=Integer", + 1<<3, + "intArray[1] <<= 3"); + } + + @Test public void testComparison() throws Exception { + assertIntercept( + "Script1.point/Script1.point/Point.equals(Point)/Integer.compareTo(Integer)", + true, + "point==point; 5==5"); + } + + @Test public void testAnonymousClass() throws Exception { + assertIntercept( + "new Script1$1(Script1)/Script1$1.@this$0=Script1/Script1$1.plusOne(Integer)/Integer.plus(Integer)", + 6, + "def x = new Object() {\n" + + " def plusOne(rhs) {\n" + + " return rhs+1\n" + + " }\n" + + "}\n" + + "x.plusOne(5)\n"); + } + + @Test public void testIssue2() throws Exception { + assertIntercept("new HashMap()/HashMap.get(String)/Script1.nop(null)",null,"def nop(v) { }; nop(new HashMap().dummy);"); + assertIntercept("Script2.nop()",null,"def nop() { }; nop();"); + assertIntercept("Script3.nop(null)",null,"def nop(v) { }; nop(null);"); + } + + @Test public void testSystemExitAsFunction() throws Exception { + assertIntercept("TheTest:idem(Integer)/TheTest:idem(Integer)",123,"org.kohsuke.groovy.sandbox.TheTest.idem(org.kohsuke.groovy.sandbox.TheTest.idem(123))"); + } + + /** + * Idempotent function used for testing + */ + public static Object idem(Object o) { + return o; + } + + @Test public void testArrayArgumentsInvocation() throws Exception { + assertIntercept( + "new TheTest$MethodWithArrayArg()/TheTest$MethodWithArrayArg.f(Object[])", + 3, + "new TheTest.MethodWithArrayArg().f(new Object[3])"); + } + + public static class MethodWithArrayArg { + public Object f(Object[] arg) { + return arg.length; + } + } + + /** + * See issue #6. We are not intercepting calls to null. + */ + @Test public void testNull() throws Exception { + assertIntercept("", NullObject.class, "def x=null; null.getClass()"); + assertIntercept("", "null3", "def x=null; x.plus('3')"); + assertIntercept("", false, "def x=null; x==3"); + } + + /** + * See issue #9 + */ + @Test public void testAnd() throws Exception { + assertIntercept("", false, + "String s = null\n" + + "if (s != null && s.length > 0)\n" + + " throw new Exception()\n" + + "return false\n"); + } + + @Test public void testLogicalNotEquals() throws Exception { + assertIntercept("Integer.toString()/String.compareTo(String)", true, + "def x = 3.toString(); if (x != '') return true; else return false;"); + } + + // see issue 8 + @Test public void testClosureDelegation() throws Exception { + assertIntercept(Arrays.asList + ( + "Script1$_run_closure1.call()", + "Script1$_run_closure1.delegate=String", + "String.length()" + ), 3, + "def x = 0\n" + + "def c = { ->\n" + + " delegate = 'foo'\n" + + " x = length()\n" + + "}\n" + + "c()\n" + + "x\n"); + } + + @Test public void testClosureDelegationOwner() throws Exception { + assertIntercept(Arrays.asList + ( + "Script1$_run_closure1.call()", + "Script1$_run_closure1.delegate=String", + "Script1$_run_closure1$_closure2.call()", + "String.length()" + ), + 3, + "def x = 0\n" + + "def c = { ->\n" + + " delegate = 'foo';\n" + + " { -> x = length() }()\n" + + "}\n" + + "c()\n" + + "x\n"); + } + + @Test public void testClosureDelegationProperty() throws Exception { + // TODO: ideally we should be seeing String.length() + // doing so requires a call site selection and deconstruction + assertIntercept(Arrays.asList + ( + "Script1$_run_closure1.call()", + "new SomeBean(Integer,Integer)", + "Script1$_run_closure1.delegate=SomeBean", + // by the default delegation rule of Closure, it first attempts to get Script1.x, + // and only after we find out that there's no such property, we fall back to SomeBean.x + "Script1.x", + "SomeBean.x", + "Script1.y", + "SomeBean.y", + "Integer.plus(Integer)" + ), + 3, + "def sum = 0\n" + + "def c = { ->\n" + + " delegate = new SomeBean(1,2)\n" + + " sum = x+y\n" + + "}\n" + + "c()\n" + + "sum\n"); + } + + @Test public void testClosureDelegationPropertyDelegateOnly() throws Exception { + assertIntercept(Arrays.asList + ( + "Script1$_run_closure1.call()", + "new SomeBean(Integer,Integer)", + "Script1$_run_closure1.delegate=SomeBean", + "Script1$_run_closure1.resolveStrategy=Integer", + // with DELEGATE_FIRST rule, unlike testClosureDelegationProperty() it shall not touch Script1.* + "SomeBean.x", + "SomeBean.y", + "Integer.plus(Integer)" + ), + 3, + "def sum = 0\n" + + "def c = { ->\n" + + " delegate = new SomeBean(1,2)\n" + + " resolveStrategy = 1; // Closure.DELEGATE_FIRST\n" + + " sum = x+y\n" + + "}\n" + + "c()\n" + + "sum\n"); + } + + @Test public void testClosureDelegationPropertyOwner() throws Exception { + /* + The way property access of 'x' gets dispatched to is: + + innerClosure.getProperty("x"), which delegates to its owner, which is + outerClosure.getProperty("x"), which delegates to its delegate, which is + SomeBean.x + */ + assertIntercept(Arrays.asList + ( + "Script1$_run_closure1.call()", + "new SomeBean(Integer,Integer)", + "Script1$_run_closure1.delegate=SomeBean", + "Script1$_run_closure1$_closure2.call()", + "Script1.x", + "SomeBean.x", + "Script1.y", + "SomeBean.y", + "Integer.plus(Integer)" + ), + 3, + "def sum = 0\n" + + "def c = { ->\n" + + " delegate = new SomeBean(1,2);\n" + + " { -> sum = x+y; }()\n" + + "}\n" + + "c()\n" + + "sum\n"); + } + + @Test public void testGString() throws Exception { + assertIntercept("Integer.plus(Integer)/Integer.plus(Integer)/GStringImpl.toString()", "answer=6", + "def x = /answer=${1+2+3}/; x.toString()"); + } + + @Test public void testClosurePropertyAccess() throws Exception { + assertIntercept(Arrays.asList( + "Script1$_run_closure1.call()", + "new Exception(String)", + "Script1$_run_closure1.delegate=Exception", + "Script1.message", + "Exception.message"), + "foo", + "{ ->\n" + + " delegate = new Exception('foo')\n" + + " return message\n" + + "}()\n"); + } + + /** + * Calling method on Closure that's not delegated to somebody else. + */ + @Test public void testNonDelegatingClosure() throws Exception { + assertIntercept(Arrays.asList( + "Script1$_run_closure1.hashCode()", + "Script1$_run_closure1.equals(Script1$_run_closure1)" + ), true, + "def c = { -> }\n" + + "c.hashCode()\n" + + "c.equals(c)\n"); + + // but these guys are not on closure + assertIntercept(Arrays.asList( + "Script2$_run_closure1.call()", + "Script2$_run_closure1.hashCode()", + "Script2$_run_closure1.hashCode()", + "Integer.compareTo(Integer)" + ), true, + "def c = { ->\n" + + " hashCode()\n" + + "}\n" + + "return c()==c.hashCode()\n"); + } + + // Groovy doesn't allow this? +// void testLocalClass() { +// assertIntercept( +// "new Foo()/Foo.plusOne(Integer)/Integer.plus(Integer)", +// 7, +//""" +//class Foo { +// def plusTwo(rhs) { +// class Bar { def plusOne(rhs) { rhs + 2; } } +// return new Bar().plusOne(rhs)+1; +// } +//} +//new Foo().plusTwo(5) +//""") +// } + + // bug 14 + @Test public void testUnclassifiedStaticMethod() throws Exception { + assertIntercept(Arrays.asList + ( + "Script1.m()", + "System:getProperty(String)" + ),null, + "m()\n" + + "def m() {\n" + + " System.getProperty('foo')\n" + + "}\n"); + } + + @Test public void testInstanceOf() throws Exception { + assertIntercept("", true, + "def x = 'foo'\n" + + "x instanceof String\n"); + } + + @Test public void testRegexp() throws Exception { + assertIntercept(Arrays.asList + ( + "ScriptBytecodeAdapter:findRegex(String,String)", + "ScriptBytecodeAdapter:matchRegex(String,String)" + ), false, + "def x = 'foo'\n" + + "x =~ /bla/\n" + + "x ==~ /bla/\n"); + } + + @Issue("JENKINS-46088") + @Test public void testMatcherTypeAssignment() throws Exception { + assertIntercept(Arrays.asList + ( + "ScriptBytecodeAdapter:findRegex(String,String)", + "Matcher.matches()" + ), false, + "def x = 'foo'\n" + + "java.util.regex.Matcher m = x =~ /bla/\n" + + "return m.matches()\n"); + } + + @Test public void testNumericComparison() throws Exception { + assertIntercept("Integer.compareTo(Integer)", true, + "5 < 8"); + } + + @Test public void testIssue17() throws Exception { + assertIntercept("new IntRange(Boolean,Integer,Integer)", 45, + "def x = 0\n" + + "for ( i in 0..9 ) {\n" + + " x+= i\n" + + "}\n" + + "return x\n"); + } + + // issue 16 + @Test public void testPrePostfixLocalVariable() throws Exception { + assertIntercept("Integer.next()/ArrayList[Integer]", Arrays.asList(1, 0), + "def x = 0\n" + + "def y=x++\n" + + "return [x,y]"); + + assertIntercept("Integer.previous()", Arrays.asList(2, 2), + "def x = 3\n" + + "def y=--x\n" + + "return [x,y]"); + } + + @Test public void testPrePostfixArray() throws Exception { + assertIntercept(Arrays.asList( + "ArrayList[Integer]", // for reading x[1] before increment + "Integer.next()", + "ArrayList[Integer]=Integer", // for writing x[1] after increment + "ArrayList[Integer]" // for reading x[1] in the return statement + ), Arrays.asList(3, 2), + "def x = [1,2,3]\n" + + "def y=x[1]++\n" + + "return [x[1],y]"); + + assertIntercept(Arrays.asList( + "ArrayList[Integer]", // for reading x[1] before increment + "Integer.previous()", + "ArrayList[Integer]=Integer", // for writing x[1] after increment + "ArrayList[Integer]" // for reading x[1] in the return statement + ), Arrays.asList(1, 1), + "def x = [1,2,3]\n" + + "def y=--x[1]\n" + + "return [x[1],y]"); + } + + @Test public void testPrePostfixProperty() throws Exception { + assertIntercept(Arrays.asList( + "Script1.x=Integer", // x=3 + "Script1.x", + "Integer.next()", + "Script1.x=Integer", // read, plus, then write back + "Script1.x" // final read for the return statement + ), Arrays.asList(4, 3), + "x = 3\n" + + "def y=x++\n" + + "return [x,y]\n"); + + assertIntercept(Arrays.asList( + "Script2.x=Integer", // x=3 + "Script2.x", + "Integer.previous()", + "Script2.x=Integer", // read, plus, then write back + "Script2.x" // final read for the return statement + ), Arrays.asList(2, 2), + "x = 3\n" + + "def y=--x\n" + + "return [x,y]\n"); + } + + @Test public void testCatchStatement() throws Exception { + sandboxedEval( + "def o = null\n" + + "try {\n" + + " o.hello()\n" + + " return null\n" + + "} catch (Exception e) {\n" + + " throw new Exception('wrapped', e)\n" + + "}", + ShouldFail.class, + e -> { + assertThat(e.getMessage(), containsString("wrapped")); + assertThat(e.getCause(), instanceOf(NullPointerException.class)); + }); + } + + /** + * Makes sure the line number in the source code is preserved after translation. + */ + @Test public void testIssue21() throws Exception { + sandboxedEval( + "\n" + // line 1 + "def x = null\n" + + "def cl = {\n" + + " x.hello()\n" + // line 4 + "}\n" + + "try {\n" + + " cl();\n" + // line 7 + "} catch (Exception e) {\n" + + " throw new Exception('wrapped', e)\n" + + "}", + ShouldFail.class, + e -> { + assertThat(e.getMessage(), containsString("wrapped")); + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + + String s = sw.toString(); + assertThat(s, containsString("Script1.groovy:4")); + assertThat(s, containsString("Script1.groovy:7")); + }); + } + + @Test public void testIssue15() throws Exception { + sandboxedEval( + "try {\n" + + " def x = null\n" + + " return x.nullProp\n" + + "} catch (Exception e) {\n" + + " throw new Exception('wrapped', e)\n" + + "}", + ShouldFail.class, + e -> { + assertThat(e.getMessage(), containsString("wrapped")); + assertThat(e.getCause(), instanceOf(NullPointerException.class)); + }); + // x.nullProp shouldn't be intercepted + assertIntercepted("new Exception(String,NullPointerException)"); + + sandboxedEval( + "try {\n" + + " def x = null\n" + + " x.nullProp = 1\n" + + "} catch (Exception e) {\n" + + " throw new Exception('wrapped', e)\n" + + "}", + ShouldFail.class, + e -> { + assertThat(e.getMessage(), containsString("wrapped")); + assertThat(e.getCause(), instanceOf(NullPointerException.class)); + }); + // x.nullProp shouldn't be intercepted + assertIntercepted("new Exception(String,NullPointerException)"); + } + + @Test public void testInOperator() throws Exception { + assertIntercept( + "Integer.isCase(Integer)", true, "1 in 1" + ); + + assertIntercept( + "Integer.isCase(Integer)", false, "1 in 2" + ); + + assertIntercept( + "ArrayList.isCase(Integer)", true, "1 in [1]" + ); + + assertIntercept( + "ArrayList.isCase(Integer)", false, "1 in [2]" + ); + } + + /** + * Property access to Map is handled specially by MetaClassImpl, so our interceptor needs to treat that + * accordingly. + */ + @Test public void testMapPropertyAccess() throws Exception { + assertIntercept("new HashMap()/HashMap.get(String)",null,"new HashMap().dummy;"); + assertIntercept("new HashMap()/HashMap.put(String,Integer)",5,"new HashMap().dummy=5"); + } + + /** + * Intercepts super.toString() + */ + @Issue("JENKINS-42563") + @Test public void testSuperCall() throws Exception { + assertIntercept(Arrays.asList( + "new Zot()", + "new Bar()", + "new Foo()", + "Zot.toString()", + "Zot.super(Bar).toString()", + "String.plus(String)" + ), "xfoo", + "class Foo {\n" + + " public String toString() {\n" + + " return 'foo'\n" + + " }\n" + + "}\n" + + "class Bar extends Foo {\n" + + " public String toString() {\n" + + " return 'x'+super.toString()\n" + + " }\n" + + "}\n" + + "class Zot extends Bar {}\n" + + "new Zot().toString()\n"); + } + + @Test public void testPostfixOpInClosure() throws Exception { + assertIntercept(Arrays.asList( + "ArrayList.each(Script1$_run_closure1)", + "Integer.next()", + "ArrayList[Integer]", + "Integer.next()", + "ArrayList[Integer]", + "Integer.next()", + "ArrayList[Integer]", + "Integer.next()", + "ArrayList[Integer]", + "Integer.next()", + "ArrayList[Integer]"), + 5, + "def cnt = 0\n" + + "[0, 1, 2, 3, 4].each {\n" + + " cnt++\n" + + "}\n" + + "return cnt\n"); + } + + @Issue("SECURITY-566") + @Test public void testTypeCoercion() throws Exception { + Field pxyCounterField = ProxyGeneratorAdapter.class.getDeclaredField("pxyCounter"); + pxyCounterField.setAccessible(true); + AtomicLong pxyCounterValue = (AtomicLong) pxyCounterField.get(null); + pxyCounterValue.set(0); // make sure *_groovyProxy names are predictable + assertIntercept("Locale:getDefault()/Class1_groovyProxy.getDefault()", + Locale.getDefault(), + "interface I {\n" + + " Locale getDefault()\n" + + "}\n" + + "(Locale as I).getDefault()\n"); + } + + @Issue("JENKINS-33468") + @Test public void testClosureImplicitIt() throws Exception { + assertIntercept(Arrays.asList( + "Script1.c=Script1$_run_closure1", + "Script1.c(Integer)", + "Integer.plus(Integer)" + ), 2, + "c = { it + 1 }\n" + + "c(1)\n" + ); + + assertIntercept(Arrays.asList( + "Script2.c=Script2$_run_closure1", + "Script2.c(Integer)", + "Integer.plus(Integer)" + ), 2, + "c = {v -> v + 1 }\n" + + "c(1)\n" + ); + + assertIntercept(Arrays.asList( + "Script3.c=Script3$_run_closure1", + "Script3.c()" + ), 2, + "c = {-> 2 }\n" + + "c()" + ); + } + + @Issue("JENKINS-46191") + @Test public void testEmptyDeclaration() throws Exception { + assertIntercept("", + "abc", + "String a\n" + + "a = 'abc'\n" + + "return a\n"); + } + + @Issue("SECURITY-663") + @Test public void testAsFile() throws Exception { + File f = File.createTempFile("foo", ".tmp"); + + ResourceGroovyMethods.write(f, "This is\na test\n"); + assertIntercept(Arrays.asList( + "new File(String)", + "File.each(Script1$_run_closure1)", + "ArrayList.leftShift(String)", + "ArrayList.leftShift(String)", + "ArrayList.join(String)"), + "This is a test", + "def s = []\n" + + "($/" + f.getCanonicalPath() + "/$ as File).each { s << it }\n" + + "s.join(' ')\n"); + } + + @Issue("JENKINS-50380") + @Test public void testCheckedCastWhenAssignable() throws Exception { + assertIntercept("new NonArrayConstructorList(Boolean,Boolean)/NonArrayConstructorList.join(String)", + "one", + "NonArrayConstructorList foo = new NonArrayConstructorList(true, false)\n" + + "List castFoo = (List)foo\n" + + "return castFoo.join('')\n"); + } + + @Issue("JENKINS-50470") + @Test public void testCollectionGetProperty() throws Exception { + assertIntercept(Arrays.asList( + "new SimpleNamedBean(String)", + "new SimpleNamedBean(String)", + "new SimpleNamedBean(String)", + // Before the JENKINS-50470 fix, this would just be ArrayList.name + "SimpleNamedBean.name", + "SimpleNamedBean.name", + "SimpleNamedBean.name", + "ArrayList.class", + "ArrayList.join(String)", + "String.plus(String)", + "String.plus(Class)"), + "abc class java.util.ArrayList", + "def l = [new SimpleNamedBean('a'), new SimpleNamedBean('b'), new SimpleNamedBean('c')]\n" + + "def nameList = l.name\n" + + "def cl = l.class\n" + + "return nameList.join('') + ' ' + cl\n"); + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelectorTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelectorTest.java new file mode 100644 index 000000000..904b3fa0f --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/impl/GroovyCallSiteSelectorTest.java @@ -0,0 +1,43 @@ +/* + * The MIT License + * + * Copyright 2020 CloudBees, Inc. + * + * 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. + */ + +package org.kohsuke.groovy.sandbox.impl; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +public class GroovyCallSiteSelectorTest { + + @Test public void missingConstructor() { + try { + GroovyCallSiteSelector.findConstructor(GroovyCallSiteSelectorTest.class, new Object[]{ 1, 'a' }, null); + fail("Constructor should not have been found"); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("Unable to find constructor: new org.kohsuke.groovy.sandbox.impl.GroovyCallSiteSelectorTest java.lang.Integer java.lang.Character")); + } + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitSandbox.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitSandbox.java new file mode 100644 index 000000000..1486f3441 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitSandbox.java @@ -0,0 +1,17 @@ +package org.kohsuke.groovy.sandbox.no_exit; + +import org.kohsuke.groovy.sandbox.GroovyInterceptor; + +/** + * Reject any static calls to {@link System}. + * + * @author Kohsuke Kawaguchi + */ +public class NoSystemExitSandbox extends GroovyInterceptor { + @Override + public Object onStaticCall(GroovyInterceptor.Invoker invoker, Class receiver, String method, Object... args) throws Throwable { + if (receiver == System.class && method.equals("exit")) + throw new SecurityException("No call on System.exit() please"); + return super.onStaticCall(invoker, receiver, method, args); + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitTest.java new file mode 100644 index 000000000..6bfa96765 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/NoSystemExitTest.java @@ -0,0 +1,52 @@ +package org.kohsuke.groovy.sandbox.no_exit; + +import groovy.lang.GroovyShell; +import junit.framework.TestCase; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.kohsuke.groovy.sandbox.SandboxTransformer; + +/** + * + * + * @author Kohsuke Kawaguchi + */ +public class NoSystemExitTest extends TestCase { + GroovyShell sh; + NoSystemExitSandbox sandbox = new NoSystemExitSandbox(); + + @Override + protected void setUp() { + CompilerConfiguration cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new SandboxTransformer()); + sh = new GroovyShell(cc); + sandbox.register(); + } + + @Override + protected void tearDown() { + sandbox.unregister(); + } + + void assertFail(String script) { + try { + sh.evaluate(script); + fail("Should have failed"); + } catch (SecurityException e) { + // as expected + } + } + + void eval(String script) { + sh.evaluate(script); + } + + public void test1() { + assertFail("System.exit(-1)"); + assertFail("foo(System.exit(-1))"); + assertFail("System.exit(-1)==System.exit(-1)"); + assertFail("def x=System.&exit; x(-1)"); + + // but this should be OK + eval("System.getProperty('abc')"); + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/package-info.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/package-info.java new file mode 100644 index 000000000..107579569 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/no_exit/package-info.java @@ -0,0 +1,4 @@ +/** + * This test demonstrates the canonical "no System.exit() call" situation. + */ +package org.kohsuke.groovy.sandbox.no_exit; diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/Robot.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/Robot.java new file mode 100644 index 000000000..917fff6cf --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/Robot.java @@ -0,0 +1,28 @@ +package org.kohsuke.groovy.sandbox.robot; + +/** + * Robot that's exposed to a sandboxed script. + * + * Script can access all aspects of the robot except the brain, which contains a secret. + * + * @author Kohsuke Kawaguchi + */ +public class Robot { + public class Arm { + public void wave(int n) { + // wave arms N times + } + } + + public void move() {} + + public class Leg {} + + // scripts will not have access to Brain + public class Brain {} + + public final Brain brain = new Brain(); + + public final Arm leftArm = new Arm(),rightArm = new Arm(); + public final Leg leftLeg = new Leg(),rightLeg = new Leg(); +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/RobotSandbox.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/RobotSandbox.java new file mode 100644 index 000000000..8136a4001 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/RobotSandbox.java @@ -0,0 +1,36 @@ +package org.kohsuke.groovy.sandbox.robot; + +import groovy.lang.Closure; +import groovy.lang.Script; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.kohsuke.groovy.sandbox.GroovyValueFilter; + +/** + * This {@link org.kohsuke.groovy.sandbox.GroovyInterceptor} implements a security check. + * + * @author Kohsuke Kawaguchi + */ +public class RobotSandbox extends GroovyValueFilter { + @Override + public Object filter(Object o) { + if (o == null || ALLOWED_TYPES.contains(o.getClass())) + return o; + if (o instanceof Script || o instanceof Closure) + return o; // access to properties of compiled groovy script + throw new SecurityException("Oops, unexpected type: " + o.getClass()); + } + + private static final Set ALLOWED_TYPES = new HashSet<>(Arrays.asList( + Robot.class, + Robot.Arm.class, + Robot.Leg.class, + String.class, + Integer.class, + Boolean.class + // all the primitive types should be OK, but I'm too lazy + + // I'm not adding Class, which rules out all the static method calls + )); +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/RobotTest.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/RobotTest.java new file mode 100644 index 000000000..b03ae02e4 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/RobotTest.java @@ -0,0 +1,64 @@ +package org.kohsuke.groovy.sandbox.robot; + +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import junit.framework.TestCase; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.kohsuke.groovy.sandbox.SandboxTransformer; + +/** + * + * + * @author Kohsuke Kawaguchi + */ +public class RobotTest extends TestCase { + Robot robot; + GroovyShell sh; + RobotSandbox sandbox = new RobotSandbox(); + + @Override + protected void setUp() { + CompilerConfiguration cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new SandboxTransformer()); + Binding binding = new Binding(); + binding.setProperty("robot", robot = new Robot()); + sh = new GroovyShell(binding,cc); + sandbox.register(); + } + + @Override + protected void tearDown() { + sandbox.unregister(); + } + + void assertFail(String script) { + try { + sh.evaluate(script); + fail("Should have failed"); + } catch (SecurityException e) { + // as expected + } + } + + void eval(String script) { + sh.evaluate(script); + } + + public void test1() { + // these are OK + eval("robot.leftArm.wave(3)"); + eval("[robot.@leftArm,robot.@rightArm]*.wave(3)"); + eval("if (robot.leftArm!=null) robot.leftArm.wave(1)"); + eval("def c = { x -> x.leftArm.wave(3) }; c(robot);"); + + // these are not + assertFail("robot.brain"); + assertFail("robot.@brain"); + assertFail("robot['brain']"); + assertFail("System.exit(-1)"); + assertFail("def c = { -> delegate = System; exit(-1) }; c();"); + assertFail("Class.forName('java.lang.String')"); + assertFail("'foo'.class.name"); + assertFail("new java.awt.Point(1,2)"); + } +} diff --git a/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/package-info.java b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/package-info.java new file mode 100644 index 000000000..65be72088 --- /dev/null +++ b/groovy-sandbox/src/test/java/org/kohsuke/groovy/sandbox/robot/package-info.java @@ -0,0 +1,7 @@ +/** + * This test demonstrates a typical use of the sandboxing, + * where you have a set of objects that are exposed to a sandboxed groovy script for some computation. + * + * The sandboxed script can access those exposed objects, but nothing else. + */ +package org.kohsuke.groovy.sandbox.robot; diff --git a/README.md b/plugin/README.md similarity index 100% rename from README.md rename to plugin/README.md diff --git a/plugin/pom.xml b/plugin/pom.xml new file mode 100644 index 000000000..6fbf494cf --- /dev/null +++ b/plugin/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + org.jenkins-ci.plugins + script-security-parent + ${changelist} + + script-security + hpi + Script Security Plugin + https://github.com/jenkinsci/script-security-plugin + + + 2.479 + ${jenkins.baseline}.3 + true + groovy-sandbox + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + io.jenkins.tools.bom + bom-${jenkins.baseline}.x + 3893.v213a_42768d35 + import + pom + + + + + + + org.kohsuke + groovy-sandbox + ${changelist} + + + org.codehaus.groovy + groovy + + + + + io.jenkins.plugins + caffeine-api + + + io.jenkins + configuration-as-code + test + + + io.jenkins.configuration-as-code + test-harness + test + + + diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/RejectedAccessException.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/RejectedAccessException.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/RejectedAccessException.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/RejectedAccessException.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/Whitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/ClassLoaderWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/ClassLoaderWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/ClassLoaderWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/ClassLoaderWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelector.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelector.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelector.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelector.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovySandbox.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovySandbox.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovySandbox.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovySandbox.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/RejectASTTransformsCustomizer.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/RejectASTTransformsCustomizer.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/RejectASTTransformsCustomizer.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/RejectASTTransformsCustomizer.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptor.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptor.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptor.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptor.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoader.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoader.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoader.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoader.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AbstractWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AbstractWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AbstractWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AbstractWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AclAwareWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AclAwareWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AclAwareWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AclAwareWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AnnotatedWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AnnotatedWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AnnotatedWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/AnnotatedWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/BlanketWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/BlanketWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/BlanketWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/BlanketWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelist.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelist.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelist.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelist.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/Whitelisted.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/Whitelisted.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/Whitelisted.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/Whitelisted.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalListener.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalListener.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalListener.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalListener.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/Language.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/Language.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/Language.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/Language.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLink.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLink.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLink.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLink.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNote.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNote.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNote.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNote.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedClasspathException.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedClasspathException.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedClasspathException.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedClasspathException.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedUsageException.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedUsageException.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedUsageException.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/UnapprovedUsageException.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyLanguage.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyLanguage.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyLanguage.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyLanguage.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyShellLanguage.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyShellLanguage.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyShellLanguage.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyShellLanguage.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyXmlLanguage.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyXmlLanguage.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyXmlLanguage.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/GroovyXmlLanguage.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JellyLanguage.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JellyLanguage.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JellyLanguage.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JellyLanguage.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JexlLanguage.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JexlLanguage.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JexlLanguage.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/JexlLanguage.java diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/SystemCommandLanguage.java b/plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/SystemCommandLanguage.java similarity index 100% rename from src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/SystemCommandLanguage.java rename to plugin/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/SystemCommandLanguage.java diff --git a/src/main/resources/index.jelly b/plugin/src/main/resources/index.jelly similarity index 100% rename from src/main/resources/index.jelly rename to plugin/src/main/resources/index.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/Messages.properties b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/Messages.properties similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/Messages.properties rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/Messages.properties diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/JENKINS-15604.js b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/JENKINS-15604.js similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/JENKINS-15604.js rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/JENKINS-15604.js diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/config.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/config.jelly similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/config.jelly rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/config.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-classpath.html b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-classpath.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-classpath.html rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-classpath.html diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-sandbox.html b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-sandbox.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-sandbox.html rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScript/help-sandbox.html diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/blacklist b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/blacklist similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/blacklist rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/blacklist diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/generic-whitelist b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/generic-whitelist similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/generic-whitelist rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/generic-whitelist diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/jenkins-whitelist b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/jenkins-whitelist similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/jenkins-whitelist rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/jenkins-whitelist diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext/index.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext/index.jelly similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext/index.jelly rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext/index.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/config.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/config.jelly similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/config.jelly rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/config.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/help-path.html b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/help-path.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/help-path.html rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/help-path.html diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/resources.js b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/resources.js similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/resources.js rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntry/resources.js diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/header.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/header.jelly similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/header.jelly rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/header.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/validate.js b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/validate.js similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/validate.js rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/FormValidationPageDecorator/validate.js diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/config.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/config.jelly similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/config.jelly rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/config.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-hide.js b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-hide.js similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-hide.js rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-hide.js diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-show.js b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-show.js similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-show.js rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/deprecated-approvedClasspaths-clear-btn-show.js diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/help-forceSandbox.html b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/help-forceSandbox.html similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/help-forceSandbox.html rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/help-forceSandbox.html diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/script-approval.js b/plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/script-approval.js similarity index 100% rename from src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/script-approval.js rename to plugin/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/script-approval.js diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelectorTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelectorTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelectorTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyCallSiteSelectorTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyLanguageCoverageTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyLanguageCoverageTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyLanguageCoverageTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyLanguageCoverageTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyMemoryLeakTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyMemoryLeakTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyMemoryLeakTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/GroovyMemoryLeakTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoaderTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoaderTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoaderTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxResolvingClassLoaderTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelistTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelistTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelistTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/EnumeratingWhitelistTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelistTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelistTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelistTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/GenericWhitelistTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/JenkinsWhitelistTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/JenkinsWhitelistTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/JenkinsWhitelistTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/JenkinsWhitelistTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelistTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelistTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelistTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/ProxyWhitelistTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelistTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelistTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelistTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/sandbox/whitelists/StaticWhitelistTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/AbstractApprovalTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/AbstractApprovalTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/AbstractApprovalTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/AbstractApprovalTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Approvable.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Approvable.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Approvable.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Approvable.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntryTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntryTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntryTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ClasspathEntryTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/EntryApprovalTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/EntryApprovalTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/EntryApprovalTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/EntryApprovalTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/HasherScriptApprovalTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/HasherScriptApprovalTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/HasherScriptApprovalTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/HasherScriptApprovalTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/JcascTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/JcascTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/JcascTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/JcascTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Manager.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Manager.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Manager.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/Manager.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLoadingTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLoadingTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLoadingTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalLoadingTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNoteTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNoteTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNoteTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalNoteTest.java diff --git a/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest.java b/plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest.java similarity index 100% rename from src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest.java rename to plugin/src/test/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest.java diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest/all.groovy b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest/all.groovy similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest/all.groovy rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SandboxInterceptorTest/all.groovy diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/README.md b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/README.md similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/README.md rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/README.md diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/script-security-plugin-testjar.jar b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/script-security-plugin-testjar.jar similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/script-security-plugin-testjar.jar rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/script-security-plugin-testjar.jar diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/updated/script-security-plugin-testjar.jar b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/updated/script-security-plugin-testjar.jar similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/updated/script-security-plugin-testjar.jar rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/SecureGroovyScriptTest/updated/script-security-plugin-testjar.jar diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder/config.jelly b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder/config.jelly similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder/config.jelly rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/TestGroovyRecorder/config.jelly diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/somejar.jar b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/somejar.jar similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/somejar.jar rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/sandbox/groovy/somejar.jar diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/dangerousApproved.zip b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/dangerousApproved.zip similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/dangerousApproved.zip rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/dangerousApproved.zip diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/malformedScriptApproval.zip b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/malformedScriptApproval.zip similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/malformedScriptApproval.zip rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/malformedScriptApproval.zip diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/reload/scriptApproval.xml b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/reload/scriptApproval.xml similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/reload/scriptApproval.xml rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/reload/scriptApproval.xml diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/upgradeSmokes/scriptApproval.xml b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/upgradeSmokes/scriptApproval.xml similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/upgradeSmokes/scriptApproval.xml rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApprovalTest/upgradeSmokes/scriptApproval.xml diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test.yaml b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test.yaml similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test.yaml rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test.yaml diff --git a/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test_expected.yaml b/plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test_expected.yaml similarity index 100% rename from src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test_expected.yaml rename to plugin/src/test/resources/org/jenkinsci/plugins/scriptsecurity/scripts/smoke_test_expected.yaml diff --git a/pom.xml b/pom.xml index 2f67eb974..e0e9be774 100644 --- a/pom.xml +++ b/pom.xml @@ -1,91 +1,50 @@ - - 4.0.0 - - org.jenkins-ci.plugins - plugin - 6.2221.va_045130417c9 - - - - script-security - ${changelist} - hpi - Script Security Plugin - https://github.com/jenkinsci/${project.artifactId}-plugin - - 999999-SNAPSHOT - - 2.479 - ${jenkins.baseline}.3 - jenkinsci/${project.artifactId}-plugin - true - groovy-sandbox - - - - MIT License - https://opensource.org/licenses/MIT - - - - scm:git:https://github.com/${gitHubRepo}.git - scm:git:git@github.com:${gitHubRepo}.git - https://github.com/${gitHubRepo} - ${scmTag} - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - - - repo.jenkins-ci.org - https://repo.jenkins-ci.org/public/ - - - - - - - io.jenkins.tools.bom - bom-${jenkins.baseline}.x - 3893.v213a_42768d35 - import - pom - - - - - - - org.kohsuke - groovy-sandbox - 1.34.1 - - - org.codehaus.groovy - groovy - - - - - io.jenkins.plugins - caffeine-api - - - io.jenkins - configuration-as-code - test - - - io.jenkins.configuration-as-code - test-harness - test - - + + 4.0.0 + + org.jenkins-ci.plugins + plugin + 6.2221.va_045130417c9 + + + script-security-parent + ${changelist} + pom + Script Security Parent + https://github.com/jenkinsci/script-security-plugin + + + MIT License + https://opensource.org/licenses/MIT + repo + + + + groovy-sandbox + plugin + + + scm:git:https://github.com/${gitHubRepo}.git + scm:git:git@github.com:${gitHubRepo}.git + ${scmTag} + https://github.com/${gitHubRepo} + + + 999999-SNAPSHOT + jenkinsci/script-security-plugin + 2.4.21 + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + +