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 84da9c5 commit 9c41a16
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 6 deletions.
10 changes: 7 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,16 @@
</pluginRepositories>
<properties>
<changelist>999999-SNAPSHOT</changelist>
<jenkins.version>2.289.3</jenkins.version>
<jenkins.version>2.346.1</jenkins.version>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<workflow-cps-plugin.version>2803.v1a_f77ffcc773</workflow-cps-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.289.x</artifactId>
<version>1500.ve4d05cd32975</version>
<artifactId>bom-2.346.x</artifactId>
<version>1607.va_c1576527071</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand Down Expand Up @@ -101,6 +102,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>${workflow-cps-plugin.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
Expand All @@ -117,6 +119,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1184.v85d16b_d851b_3</version>
</dependency>

<dependency>
Expand Down Expand Up @@ -166,6 +169,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-cps</artifactId>
<version>${workflow-cps-plugin.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
Expand Down
45 changes: 42 additions & 3 deletions src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStep.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import java.util.logging.Logger;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import groovy.lang.MissingPropertyException;
import javax.inject.Inject;
import jenkins.model.Jenkins;
import jenkins.scm.impl.SingleSCMSource;
Expand All @@ -75,6 +76,8 @@
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.groovy.sandbox.GroovyInterceptor;
import org.kohsuke.groovy.sandbox.impl.Checker;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
Expand Down Expand Up @@ -270,11 +273,15 @@ public static final class LoadedClasses extends GroovyObjectSupport implements S
if (clazz != null) {
// Field access?
try {
// not doing a Whitelist check since GroovyClassLoaderWhitelist would be allowing it anyway
if (isSandboxed()) {
return Checker.checkedGetAttribute(loadClass(prefix + clazz), false, false, property);
}
return loadClass(prefix + clazz).getField(property).get(null);
} catch (NoSuchFieldException x) {
} catch (MissingPropertyException | NoSuchFieldException x) {
// guessed wrong
} catch (IllegalAccessException x) {
} catch (SecurityException x) {
throw x;
} catch (Throwable x) {
throw new GroovyRuntimeException(x);
}
}
Expand All @@ -284,6 +291,8 @@ public static final class LoadedClasses extends GroovyObjectSupport implements S
loadClass(prefix + fullClazz);
// OK, class really exists, stash it and await methods
return new LoadedClasses(library, trusted, changelog, prefix, fullClazz, srcUrl);
} else if (clazz != null) {
throw new MissingPropertyException(property, loadClass(prefix + clazz));
} else {
// Still selecting package components.
return new LoadedClasses(library, trusted, changelog, prefix + property + '.', null, srcUrl);
Expand All @@ -293,13 +302,43 @@ public static final class LoadedClasses extends GroovyObjectSupport implements S
@Override public Object invokeMethod(String name, Object _args) {
Class<?> c = loadClass(prefix + clazz);
Object[] args = _args instanceof Object[] ? (Object[]) _args : new Object[] {_args}; // TODO why does Groovy not just pass an Object[] to begin with?!
if (isSandboxed()) {
try {
if (name.equals("new")) {
return Checker.checkedConstructor(c, args);
} else {
return Checker.checkedStaticCall(c, name, args);
}
} catch (SecurityException x) {
throw x;
} catch (Throwable x) {
throw new GroovyRuntimeException(x);
}
}
if (name.equals("new")) {
return InvokerHelper.invokeConstructorOf(c, args);
} else {
return InvokerHelper.invokeStaticMethod(c, name, args);
}
}

/**
* Check whether the current thread has at least one active {@link GroovyInterceptor}.
* <p>
* Typically, {@code GroovyClassLoaderWhitelist} will allow access to everything defined in a class in a
* library, but there are some synthetic constructors, fields, and methods which should not be accessible.
* <p>
* As a result, when getting properties or invoking methods using this class, we need to apply sandbox
* protection if the Pipeline code performing the operation is sandbox-transformed. Unfortunately, it is
* difficult to detect that case specifically, so we instead intercept all calls if the Pipeline itself is
* sandboxed. This results in a false positive {@code RejectedAccessException} being thrown if a trusted
* library uses the {@code library} step and tries to access static fields or methods that are not permitted to
* be used in the sandbox.
*/
private static boolean isSandboxed() {
return !GroovyInterceptor.getApplicableInterceptors().isEmpty();
}

// TODO putProperty for static field set

private Class<?> loadClass(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Rule;
import org.jvnet.hudson.test.BuildWatcher;
import org.jvnet.hudson.test.Issue;
Expand Down Expand Up @@ -124,6 +125,99 @@ public class LibraryStepTest {
r.assertLogContains("using constant vs. constant", b);
}

@Test public void missingProperty() throws Exception {
sampleRepo.init();
sampleRepo.write("src/some/pkg/MyClass.groovy", "package some.pkg; class MyClass { }");
sampleRepo.git("add", "src");
sampleRepo.git("commit", "--message=init");
Folder f = r.jenkins.createProject(Folder.class, "f");
f.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration("stuff", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))));
WorkflowJob p = f.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition(
"def lib = library 'stuff@master'\n" +
"lib.some.pkg.MyClass.no_field_with_this_name\n" , true));
WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, p);
r.assertLogContains("MissingPropertyException: No such property: no_field_with_this_name for class: some.pkg.MyClass", b);
}

@Test public void reflectionInLoadedClassesIsIntercepted() throws Exception {
sampleRepo.init();
sampleRepo.write("src/some/pkg/MyThread.groovy", "package some.pkg; class MyThread extends Thread { }");
sampleRepo.git("add", "src");
sampleRepo.git("commit", "--message=init");
Folder f = r.jenkins.createProject(Folder.class, "f");
f.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration("stuff", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))));
WorkflowJob p = f.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition(
"def lib = library 'stuff@master'\n" +
"catchError() { lib.some.pkg.MyThread.new(null) }\n" +
"catchError() { lib.some.pkg.MyThread.__$stMC }\n" +
"catchError() { lib.some.pkg.MyThread.$getCallSiteArray() }\n" , true));
WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, p);
r.assertLogContains("Rejecting illegal call to synthetic constructor", b);
r.assertLogContains("staticField some.pkg.MyThread __$stMC", b);
r.assertLogContains("staticMethod some.pkg.MyThread $getCallSiteArray", b);
}

@Issue("SECURITY-2824")
@Test public void constructorInvocationInLoadedClassesIsIntercepted() throws Exception {
sampleRepo.init();
sampleRepo.write("src/pkg/Superclass.groovy",
"package pkg;\n" +
"class Superclass { Superclass(String x) { } }\n");
sampleRepo.write("src/pkg/Subclass.groovy",
"package pkg;\n" +
"class Subclass extends Superclass {\n" +
" def wrapper\n" +
" Subclass() { super('secret.key'); def $cw = $cw; wrapper = $cw }\n" +
"}\n");
sampleRepo.write("src/pkg/MyFile.groovy",
"package pkg;\n" +
"class MyFile extends File {\n" +
" MyFile(String path) {\n" +
" super(path)\n" +
" }\n" +
"}\n");
sampleRepo.git("add", "src");
sampleRepo.git("commit", "--message=init");
Folder f = r.jenkins.createProject(Folder.class, "f");
f.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration("stuff", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))));
WorkflowJob p = f.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition(
"def lib = library 'stuff@master'\n" +
"def wrapper = lib.pkg.Subclass.new().wrapper\n" +
"def file = lib.pkg.MyFile.new(wrapper, 'unused')\n" +
"echo(/${[file, file.class]}/)", true));
WorkflowRun b = r.buildAndAssertStatus(Result.FAILURE, p);
r.assertLogContains("Rejecting illegal call to synthetic constructor: private pkg.MyFile", b);
}

@Ignore("Trusted libraries should never get a RejectedAccessException, but this case should be uncommon and is difficult to handle more precisely")
@Test public void falsePositiveRejectedAccessExceptionInTrustedLibrary() throws Exception {
sampleRepo.init();
sampleRepo.git("branch", "myBranch");
sampleRepo.write("vars/doStuff.groovy",
"def call() {\n" +
" def lib = library('stuff2@myBranch')\n" +
" lib.some.pkg.MyClass.$getCallSiteArray()\n" +
"}\n");
sampleRepo.git("add", ".");
sampleRepo.git("commit", "--message=init");
sampleRepo.git("checkout", "myBranch");
sampleRepo.write("src/some/pkg/MyClass.groovy", "package some.pkg; class MyClass { }");
sampleRepo.git("add", ".");
sampleRepo.git("commit", "--message=myBranch");
GlobalLibraries.get().setLibraries(Arrays.asList(
new LibraryConfiguration("stuff", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))),
new LibraryConfiguration("stuff2", new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)))));
WorkflowJob p = r.createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition(
"@Library('stuff@master')\n" +
"import doStuff\n" +
"doStuff()\n", true));
WorkflowRun b = r.buildAndAssertSuccess(p);
}

@Test public void classesFromWrongPlace() throws Exception {
sampleRepo.init();
sampleRepo.write("src/some/pkg/Lib.groovy", "package some.pkg; class Lib {static void m() {}}");
Expand Down

0 comments on commit 9c41a16

Please sign in to comment.