Skip to content

Commit

Permalink
[SECURITY-2824]
Browse files Browse the repository at this point in the history
  • Loading branch information
dwnusbaum committed Oct 14, 2022
1 parent 5ea6281 commit 1af77ff
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 13 deletions.
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<jenkins.version>2.346.1</jenkins.version>
<no-test-jar>false</no-test-jar>
<groovy-cps.version>1.32</groovy-cps.version>
<groovy-cps.version>1.34</groovy-cps.version>
<node.version>16.17.0</node.version>
<npm.version>8.18.0</npm.version>
</properties>
Expand Down Expand Up @@ -107,6 +107,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1184.v85d16b_d851b_3</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@ private Script doParse(GroovyCodeSource codeSource) throws CompilationFailedExce
try (GroovySandbox.Scope scope = sandbox.enter()) {
if (execution != null) {
try (CpsFlowExecution.Timing t = execution.time(CpsFlowExecution.TimingKind.parse)) {
return super.parse(codeSource);
return scope.parse(CpsGroovyShell.this, codeSource);
}
} else {
return super.parse(codeSource);
return scope.parse(CpsGroovyShell.this, codeSource);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,11 @@ public Object evaluate(File file) throws CompilationFailedException, IOException

@Override
public void run(File file, String[] arguments) throws CompilationFailedException, IOException {
$getShell().run(file,arguments);
// GroovyShell.run has a bunch of weird cases related to JUnit and other stuff that we cannot safely support
// without a lot of extra work, so we just approximate its behavior. Regardless, I assume that this method is
// essentially unused since it takes a File and it is not allowed by CpsWhitelist (unlike evaluate).
$getShell().getContext().setProperty("args", arguments);
evaluate(file);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist;
Expand Down Expand Up @@ -41,35 +42,85 @@ private boolean permits(Class<?> declaringClass) {
}

@Override public boolean permitsMethod(Method method, Object receiver, Object[] args) {
return permits(method.getDeclaringClass()) || delegate.permitsMethod(method, receiver, args);
return permits(method.getDeclaringClass()) && !isIllegalSyntheticMethod(method) || delegate.permitsMethod(method, receiver, args);
}

@Override public boolean permitsConstructor(Constructor<?> constructor, Object[] args) {
return permits(constructor.getDeclaringClass()) || delegate.permitsConstructor(constructor, args);
return permits(constructor.getDeclaringClass()) && !isIllegalSyntheticConstructor(constructor) || delegate.permitsConstructor(constructor, args);
}

@Override public boolean permitsStaticMethod(Method method, Object[] args) {
return permits(method.getDeclaringClass()) || delegate.permitsStaticMethod(method, args);
return permits(method.getDeclaringClass()) && !isIllegalSyntheticMethod(method) || delegate.permitsStaticMethod(method, args);
}

@Override public boolean permitsFieldGet(Field field, Object receiver) {
return permits(field.getDeclaringClass()) || delegate.permitsFieldGet(field, receiver);
return permits(field.getDeclaringClass()) && !isIllegalSyntheticField(field) || delegate.permitsFieldGet(field, receiver);
}

@Override public boolean permitsFieldSet(Field field, Object receiver, Object value) {
return permits(field.getDeclaringClass()) || delegate.permitsFieldSet(field, receiver, value);
return permits(field.getDeclaringClass()) && !isIllegalSyntheticField(field) || delegate.permitsFieldSet(field, receiver, value);
}

@Override public boolean permitsStaticFieldGet(Field field) {
return permits(field.getDeclaringClass()) || delegate.permitsStaticFieldGet(field);
return permits(field.getDeclaringClass()) && !isIllegalSyntheticField(field) || delegate.permitsStaticFieldGet(field);
}

@Override public boolean permitsStaticFieldSet(Field field, Object value) {
return permits(field.getDeclaringClass()) || delegate.permitsStaticFieldSet(field, value);
return permits(field.getDeclaringClass()) && !isIllegalSyntheticField(field) || delegate.permitsStaticFieldSet(field, value);
}

@Override public String toString() {
return super.toString() + "[" + delegate + "]";
}

/**
* Checks whether a given field was created by the Groovy compiler and should be inaccessible even if it is
* declared by a class defined by one of the specified class loaders.
*/
private static boolean isIllegalSyntheticField(Field field) {
if (!field.isSynthetic()) {
return false;
}
Class<?> declaringClass = field.getDeclaringClass();
Class<?> enclosingClass = declaringClass.getEnclosingClass();
if (field.getType() == enclosingClass && field.getName().startsWith("this$")) {
// Synthetic field added to inner classes to reference the outer class.
return false;
} else if (declaringClass.isEnum() && Modifier.isStatic(field.getModifiers()) && field.getName().equals("$VALUES")) {
// Synthetic field added to enum classes to hold the enum constants.
return false;
}
return true;
}

/**
* Checks whether a given method was created by the Groovy compiler and should be inaccessible even if it is
* declared by a class defined by one of the specified class loaders.
*/
private static boolean isIllegalSyntheticMethod(Method method) {
if (!method.isSynthetic()) {
return false;
} else if (Modifier.isStatic(method.getModifiers()) && method.getDeclaringClass().isEnum() && method.getName().equals("$INIT")) {
// Synthetic method added to enum classes used to initialize the enum constants.
return false;
}
return true;
}

/**
* Checks whether a given constructor was created by the Groovy compiler (or groovy-sandbox) and
* should be inaccessible even if it is declared by a class defined by the specified class loader.
*/
private static boolean isIllegalSyntheticConstructor(Constructor constructor) {
if (!constructor.isSynthetic()) {
return false;
}
Class<?> declaringClass = constructor.getDeclaringClass();
Class<?> enclosingClass = declaringClass.getEnclosingClass();
if (enclosingClass != null && constructor.getParameters().length > 0 && constructor.getParameterTypes()[0] == enclosingClass) {
// Synthetic constructor added by Groovy to anonymous classes.
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import java.io.Serializable;
import java.util.Collections;
import java.util.Set;

import java.util.logging.Level;

import jenkins.model.Jenkins;
Expand Down Expand Up @@ -257,7 +256,7 @@ public void traitsSandbox() throws Exception {
WorkflowRun b = job.scheduleBuild2(0).get();
assertNull(jenkins.jenkins.getSystemMessage());
jenkins.assertBuildStatus(Result.FAILURE, b);
jenkins.assertLogContains("org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use staticMethod jenkins.model.Jenkins getInstance", b);
jenkins.assertLogContains("org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use method groovy.lang.GroovyObject invokeMethod java.lang.String java.lang.Object (org.jenkinsci.plugins.workflow.cps.CpsClosure2 getInstance)", b);
return null;
});
// Some safe idioms:
Expand Down Expand Up @@ -853,6 +852,81 @@ public void scriptInitializerCallsCpsTransformedMethod() throws Exception {
assertNull(Jenkins.get().getDescription());
}

@Issue("SECURITY-2824")
@Test public void blockCastsPropertiesAndAttributes() throws Exception {
// Instance property
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
p.setDefinition(new CpsFlowDefinition(
"class Test {\n" +
" File file\n" +
"}\n" +
"def t = new Test()\n" +
"t.file = ['secret.key']\n", true));
WorkflowRun b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use new java.io.File java.lang.String", b);
// Static property
p.setDefinition(new CpsFlowDefinition(
"class Test {\n" +
" static File file\n" +
"}\n" +
"Test.file = ['secret.key']\n", true));
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use new java.io.File java.lang.String", b);
// Instance attribute
p.setDefinition(new CpsFlowDefinition(
"class Test {\n" +
" File file\n" +
"}\n" +
"def t = new Test()\n" +
"t.@file = ['secret.key']\n", true));
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use new java.io.File java.lang.String", b);
// Static attribute
p.setDefinition(new CpsFlowDefinition(
"class Test {\n" +
" static File file\n" +
"}\n" +
"Test.@file = ['secret.key']\n", true));
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use new java.io.File java.lang.String", b);
}

@Issue("JENKINS-33023")
@Test public void groovyEnums() throws Exception {
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
p.setDefinition(new CpsFlowDefinition(
"enum Thing {\n" +
" ONE, TWO\n" +
" Thing() { }\n" +
"}\n" +
"Thing.ONE\n", true));
WorkflowRun b = jenkins.buildAndAssertSuccess(p);
p.setDefinition(new CpsFlowDefinition(
"enum Thing {\n" +
" ONE, TWO\n" +
"}\n" +
"Thing.ONE\n", true));
// Seems undesirable, but this is the current behavior. Requires new java.util.LinkedHashMap and staticMethod ImmutableASTTransformation checkPropNames.
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use new java.util.LinkedHashMap", b);
}

@Test public void blockSyntheticFieldsAndMethods() throws Throwable {
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
p.setDefinition(new CpsFlowDefinition("$getStaticMetaClass()", true));
WorkflowRun b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use method WorkflowScript $getStaticMetaClass", b);
p.setDefinition(new CpsFlowDefinition("getClass().$getCallSiteArray()", true));
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use staticMethod WorkflowScript $getCallSiteArray", b);
p.setDefinition(new CpsFlowDefinition("class Test { }; new Test().metaClass", true));
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use method groovy.lang.GroovyObject getMetaClass", b);
p.setDefinition(new CpsFlowDefinition("class Test { }; new Test().@metaClass", true));
b = jenkins.buildAndAssertStatus(Result.FAILURE, p);
jenkins.assertLogContains("Scripts not permitted to use field Test metaClass", b);
}

public static class UnsafeParameterStep extends Step implements Serializable {
private final UnsafeDescribable val;
@DataBoundConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package org.jenkinsci.plugins.workflow.cps;

import hudson.model.Result;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
import org.junit.ClassRule;

import org.junit.Test;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;

public class CpsScriptTest {

@ClassRule public static BuildWatcher watcher = new BuildWatcher();
@ClassRule public static JenkinsRule r = new JenkinsRule();

/**
Expand Down Expand Up @@ -50,4 +54,34 @@ public void evaluateShallSandbox() throws Exception {
r.buildAndAssertSuccess(p);
}

@Issue("SECURITY-2428")
@Test public void blockImplicitCastingInEvaluate() throws Exception {
AtomicInteger counter = new AtomicInteger();
BiFunction<String, String, String> embeddedScript = (decl, main) -> "" +
"class Test" + counter.incrementAndGet() + " {\\n" +
" " + decl + "\\n" +
" Object map\\n" +
" @NonCPS public void main(String[] args) { " + main + " }\\n" +
"}\\n";
WorkflowJob p = r.createProject(WorkflowJob.class);
p.setDefinition(new CpsFlowDefinition(
"list = ['secret.key']\n" +
"map = [:]\n" +
"evaluate('" + embeddedScript.apply("File list", "map.file = list") + "')\n" +
"file = map.file\n" +
"evaluate('" + embeddedScript.apply("String[] file", "map.lines = file") + "')\n" +
"for (String line in map.lines) { echo(line) }\n", true));
WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, p);
r.assertLogContains("Scripts not permitted to use new java.io.File java.lang.String", b);
}

@Test public void blockRun() throws Exception {
WorkflowJob p = r.createProject(WorkflowJob.class);
p.setDefinition(new CpsFlowDefinition("run(null, ['test'] as String[])\n", true));
WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, p);
// The existence of CpsScript.run leads me to believe that it was intended to be allowed by CpsWhitelist, but
// that is not currently the case, and I see no reason to start allowing it at this point.
r.assertLogContains("Scripts not permitted to use method groovy.lang.Script run java.io.File java.lang.String[]", b);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,12 @@ public class CpsVmExecutorServiceTest {
r.assertLogNotContains(CpsVmExecutorService.mismatchMessage("java.util.LinkedHashMap", "action", "org.jenkinsci.plugins.workflow.cps.CpsClosure2", "call"), b);
}

@Test public void wrongCatcherAsBoolean() throws Exception {
p.setDefinition(new CpsFlowDefinition("class C { def asBoolean() { 'never used' } }; if (new C()) { println('casted') } else { println('see what') }", true));
WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, p);
r.assertLogContains(CpsVmExecutorService.mismatchMessage("org.codehaus.groovy.runtime.ScriptBytecodeAdapter", "castToType", "C", "asBoolean"), b);
r.assertLogContains("java.lang.IllegalStateException: C.asBoolean must be @NonCPS; see: https://jenkins.io/redirect/pipeline-cps-method-mismatches/", b);
r.assertLogNotContains("see what", b);
}

}

0 comments on commit 1af77ff

Please sign in to comment.