Skip to content

Commit d0aa333

Browse files
dwnusbaumPldi23
authored andcommitted
SECURITY-1951
1 parent 9521045 commit d0aa333

File tree

5 files changed

+87
-7
lines changed

5 files changed

+87
-7
lines changed

src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStep.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import hudson.model.ItemGroup;
3838
import hudson.model.Run;
3939
import hudson.model.TaskListener;
40+
import hudson.scm.SCM;
4041
import hudson.security.AccessControlled;
4142
import java.io.File;
4243
import java.io.IOException;
@@ -60,6 +61,7 @@
6061
import edu.umd.cs.findbugs.annotations.NonNull;
6162
import javax.inject.Inject;
6263
import jenkins.model.Jenkins;
64+
import jenkins.scm.impl.SingleSCMSource;
6365
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
6466
import org.codehaus.groovy.runtime.InvokerHelper;
6567
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.AbstractWhitelist;
@@ -192,6 +194,18 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
192194
} else if (version == null) {
193195
throw new AbortException("Must specify a version for library " + name);
194196
}
197+
// When a user specifies a non-null retriever, they may be using SCMVar in its configuration,
198+
// so we need to run MultibranchScmRevisionVerifier to prevent unsafe behavior.
199+
// SCMVar would typically be used with SCMRetriever, but it is also possible to use it with SCMSourceRetriever and SingleSCMSource.
200+
// There may be false-positive rejections if a Multibranch Pipeline for the repo of a Pipeline library
201+
// uses the library step with a non-null retriever to check out a static version of the library.
202+
// Fixing this would require us being able to detect usage of SCMVar precisely, which is not currently possible.
203+
else if (retriever instanceof SCMRetriever) {
204+
verifyRevision(((SCMRetriever) retriever).getScm(), name);
205+
} else if (retriever instanceof SCMSourceRetriever && ((SCMSourceRetriever) retriever).getScm() instanceof SingleSCMSource) {
206+
verifyRevision(((SingleSCMSource) ((SCMSourceRetriever) retriever).getScm()).getScm(), name);
207+
}
208+
195209
LibraryRecord record = new LibraryRecord(name, version, trusted, changelog, cachingConfiguration, source);
196210
LibrariesAction action = run.getAction(LibrariesAction.class);
197211
if (action == null) {
@@ -219,6 +233,12 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
219233
return new LoadedClasses(name, record.getDirectoryName(), trusted, changelog, run);
220234
}
221235

236+
private void verifyRevision(SCM scm, String name) throws IOException, InterruptedException {
237+
for (LibraryStepRetrieverVerifier revisionVerifier : LibraryStepRetrieverVerifier.all()) {
238+
revisionVerifier.verify(this.run, listener, scm, name);
239+
}
240+
}
241+
222242
}
223243

224244
public static final class LoadedClasses extends GroovyObjectSupport implements Serializable {

src/main/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetrieverVerifier.java renamed to src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStepRetrieverVerifier.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
import java.io.IOException;
1212

1313
@Restricted(NoExternalUse.class)
14-
public interface SCMSourceRetrieverVerifier extends ExtensionPoint {
14+
public interface LibraryStepRetrieverVerifier extends ExtensionPoint {
1515

1616
void verify(Run<?, ?> run, TaskListener listener, SCM scm, String name) throws IOException, InterruptedException;
1717

18-
static ExtensionList<SCMSourceRetrieverVerifier> all() {
19-
return ExtensionList.lookup(SCMSourceRetrieverVerifier.class);
18+
static ExtensionList<LibraryStepRetrieverVerifier> all() {
19+
return ExtensionList.lookup(LibraryStepRetrieverVerifier.class);
2020
}
2121
}

src/main/java/org/jenkinsci/plugins/workflow/libs/MultibranchScmRevisionVerifier.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jenkinsci.plugins.workflow.libs;
22

3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
34
import hudson.AbortException;
45
import hudson.model.Job;
56
import hudson.model.Run;
@@ -17,7 +18,10 @@
1718
import java.io.IOException;
1819

1920
@OptionalExtension(requirePlugins={"workflow-multibranch"})
20-
public class MultibranchScmRevisionVerifier implements SCMSourceRetrieverVerifier {
21+
public class MultibranchScmRevisionVerifier implements LibraryStepRetrieverVerifier {
22+
23+
@SuppressFBWarnings("MS_SHOULD_BE_FINAL") // For script console and tests.
24+
public static boolean DISABLED = Boolean.getBoolean(MultibranchScmRevisionVerifier.class.getName() + ".DISABLED");
2125

2226
/**
2327
* Abort library retrieval if the specified build is from a Multibranch Pipeline configured to build the library's SCM and the revision being built is untrusted.
@@ -26,6 +30,9 @@ public class MultibranchScmRevisionVerifier implements SCMSourceRetrieverVerifie
2630
*/
2731
@Override
2832
public void verify(Run<?, ?> run, TaskListener listener, SCM libraryScm, String name) throws IOException, InterruptedException {
33+
if (DISABLED) {
34+
return;
35+
}
2936
// Adapted from ReadTrustedStep
3037
Job<?, ?> job = run.getParent();
3138
BranchJobProperty property = job.getProperty(BranchJobProperty.class);

src/main/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetriever.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,6 @@ private static <T> T retrySCMOperation(TaskListener listener, Callable<T> task)
174174

175175
@SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", justification = "apparently bogus complaint about redundant nullcheck in try-with-resources")
176176
static void doRetrieve(String name, boolean changelog, @NonNull SCM scm, String libraryPath, FilePath target, Run<?, ?> run, TaskListener listener) throws Exception {
177-
for (SCMSourceRetrieverVerifier revisionVerifier : SCMSourceRetrieverVerifier.all()) {
178-
revisionVerifier.verify(run, listener, scm, name);
179-
}
180177
// Adapted from CpsScmFlowDefinition:
181178
SCMStep delegate = new GenericSCMStep(scm);
182179
delegate.setPoll(false); // TODO we have no API for determining if a given SCMHead is branch-like or tag-like; would we want to turn on polling if the former?

src/test/java/org/jenkinsci/plugins/workflow/libs/SCMRetrieverTest.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,62 @@ public class SCMRetrieverTest {
124124
r.assertLogContains("Library '" + libraryName + "' has been modified in an untrusted revision", run);
125125
}
126126

127+
@Test public void libraryCanBeRetrievedStaticallyEvenWhenPipelineScmUntrusted() throws Exception {
128+
sampleRepo.init();
129+
sampleRepo.write("vars/greet.groovy", "def call(recipient) {echo(/hello from $recipient/)}");
130+
sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'master'}}");
131+
sampleRepo.write("Jenkinsfile", "greet(pkg.Clazz.whereAmI())"); // Library loaded implicitly.
132+
sampleRepo.git("add", "vars", "src", "Jenkinsfile");
133+
sampleRepo.git("commit", "--message=init");
134+
135+
sampleRepo.git("checkout", "-b", "fork");
136+
sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'fork'}}");
137+
sampleRepo.git("commit", "--all", "--message=branching");
138+
139+
WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mp");
140+
String libraryName = "stuff";
141+
LibraryConfiguration config = new LibraryConfiguration(libraryName, new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)));
142+
config.setDefaultVersion("master");
143+
config.setImplicit(true);
144+
GlobalLibraries.get().setLibraries(Collections.singletonList(config));
145+
146+
SCMSource warySource = new WarySource(sampleRepo.toString());
147+
mp.getSourcesList().add(new BranchSource(warySource));
148+
WorkflowJob job = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "fork");
149+
r.waitUntilNoActivity();
150+
WorkflowRun run = job.getLastBuild();
151+
// The fork is untrusted, but that doesn't matter because we are using stuff@master, which the untrusted user can't modify.
152+
r.assertBuildStatus(Result.SUCCESS, run);
153+
r.assertLogContains("hello from master", run);
154+
}
155+
156+
@Issue("SECURITY-1951")
157+
@Test public void libraryCantBeRetrievedWithoutVersionUsingScmSourceRetriever() throws Exception {
158+
sampleRepo.init();
159+
sampleRepo.write("vars/greet.groovy", "def call(recipient) {echo(/hello to $recipient/)}");
160+
sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'master'}}");
161+
sampleRepo.write("Jenkinsfile", "def lib = library(identifier: 'stuff@master', retriever: modernSCM(fromScm(name: 'master', scm: scm))); greet(lib.pkg.Clazz.whereAmI())");
162+
sampleRepo.git("add", "vars", "src", "Jenkinsfile");
163+
sampleRepo.git("commit", "--message=init");
164+
165+
sampleRepo.git("checkout", "-b", "fork");
166+
sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'fork'}}");
167+
sampleRepo.git("commit", "--all", "--message=branching");
168+
169+
WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mp");
170+
String libraryName = "stuff";
171+
mp.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration(libraryName, new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))))));
172+
173+
SCMSource warySource = new WarySource(sampleRepo.toString());
174+
mp.getSourcesList().add(new BranchSource(warySource));
175+
WorkflowJob job = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "fork");
176+
r.waitUntilNoActivity();
177+
WorkflowRun run = job.getLastBuild();
178+
179+
r.assertBuildStatus(Result.FAILURE, run);
180+
r.assertLogContains("Library '" + libraryName + "' has been modified in an untrusted revision", run);
181+
}
182+
127183
public static class WarySource extends GitSCMSource {
128184

129185
public WarySource(String remote) {

0 commit comments

Comments
 (0)