Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio
}
build.addAction(new SCMRevisionAction(scmSource, tip));
}
SCMRevision trusted = scmSource.getTrustedRevision(tip, listener);
SCMRevision trusted = SCMBinder.getTrustedRevision(scmSource, tip, listener, build);
boolean trustCheck = !tip.equals(trusted);
String untrustedFile = null;
String content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,36 @@
package org.jenkinsci.plugins.workflow.multibranch;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.AbortException;
import hudson.Extension;
import hudson.Functions;
import hudson.MarkupText;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotator;
import hudson.console.ConsoleNote;
import hudson.model.Action;
import hudson.model.Cause;
import hudson.model.Descriptor;
import hudson.model.DescriptorVisibilityFilter;
import hudson.model.ItemGroup;
import hudson.model.Queue;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.scm.SCM;
import hudson.triggers.SCMTrigger;
import hudson.triggers.TimerTrigger;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import jenkins.branch.Branch;
import jenkins.branch.BranchEventCause;
import jenkins.branch.BranchIndexingCause;
import jenkins.scm.api.SCMFileSystem;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import jenkins.util.SystemProperties;
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition;
import org.jenkinsci.plugins.workflow.cps.replay.ReplayCause;
import org.jenkinsci.plugins.workflow.flow.FlowDefinition;
import org.jenkinsci.plugins.workflow.flow.FlowDefinitionDescriptor;
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
Expand All @@ -61,7 +68,11 @@
class SCMBinder extends FlowDefinition {

/** Kill switch for JENKINS-33273 in case of problems. */
static /* not final */ boolean USE_HEAVYWEIGHT_CHECKOUT = Boolean.getBoolean(SCMBinder.class.getName() + ".USE_HEAVYWEIGHT_CHECKOUT"); // TODO 2.4+ use SystemProperties
static /* not final */ boolean USE_HEAVYWEIGHT_CHECKOUT = SystemProperties.getBoolean(SCMBinder.class.getName() + ".USE_HEAVYWEIGHT_CHECKOUT");

/** Kill switch for making this as strict as {@link ReadTrustedStep} about untrusted modifications. */
static /* not final */ boolean IGNORE_UNTRUSTED_EDITS = SystemProperties.getBoolean(SCMBinder.class.getName() + ".IGNORE_UNTRUSTED_EDITS");

private String scriptPath = WorkflowBranchProjectFactory.SCRIPT;

public Object readResolve() {
Expand Down Expand Up @@ -100,7 +111,7 @@ public SCMBinder(String scriptPath) {
SCM scm;
if (tip != null) {
build.addAction(new SCMRevisionAction(scmSource, tip));
SCMRevision rev = scmSource.getTrustedRevision(tip, listener);
SCMRevision rev = getTrustedRevision(scmSource, tip, listener, build);
try (SCMFileSystem fs = USE_HEAVYWEIGHT_CHECKOUT ? null : SCMFileSystem.of(scmSource, head, rev)) {
if (fs != null) { // JENKINS-33273
String script = null;
Expand All @@ -111,10 +122,10 @@ public SCMBinder(String scriptPath) {
listener.error("Could not do lightweight checkout, falling back to heavyweight").println(Functions.printThrowable(x).trim());
}
if (script != null) {
if (!rev.equals(tip)) {
// Print a warning in builds where an untrusted contributor has tried to edit Jenkinsfile.
// If we fail to check this (e.g., due to heavyweight checkout), a warning will still be printed to the log
// by the SCM, but that is less apparent.
if (!IGNORE_UNTRUSTED_EDITS && !rev.equals(tip)) {
// Make a best effort to abort builds where an untrusted contributor has tried to edit Jenkinsfile.
// If we fail to check this (e.g., due to heavyweight checkout), a warning will be printed to the log
// and the build will continue with the trusted variant, which is safe but confusing.
SCMFileSystem tipFS = SCMFileSystem.of(scmSource, head, tip);
if (tipFS != null) {
String tipScript = null;
Expand All @@ -124,9 +135,8 @@ public SCMBinder(String scriptPath) {
listener.error("Could not compare lightweight checkout of trusted revision").println(Functions.printThrowable(x).trim());
}
if (tipScript != null && !script.equals(tipScript)) {
listener.annotate(new WarningNote());
listener.getLogger().println(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis(scriptPath));
// TODO JENKINS-45970 consider aborting instead, at least optionally
build.setResult(Result.NOT_BUILT);
throw new AbortException(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis(scriptPath));
}
}
}
Expand All @@ -143,6 +153,24 @@ public SCMBinder(String scriptPath) {
return new CpsScmFlowDefinition(scm, scriptPath).create(handle, listener, actions);
}

private static Set<Class<? extends Cause>> passiveCauses = Set.of(
BranchIndexingCause.class,
BranchEventCause.class,
SCMTrigger.SCMTriggerCause.class,
TimerTrigger.TimerTriggerCause.class);
/**
* Like {@link SCMSource#getTrustedRevision} but only for builds with known passive triggers such as {@link BranchIndexingCause}.
* Other causes such as {@link Cause.UserIdCause} or {@link ReplayCause} or {@code CheckRunGHEventSubscriber.GitHubChecksRerunActionCause}
* are assumed trusted and so the tip revision is returned as is without consulting the SCM.
*/
static SCMRevision getTrustedRevision(SCMSource source, SCMRevision revision, TaskListener listener, Run<?, ?> build) throws IOException, InterruptedException {
if (build.getCauses().stream().anyMatch(c -> passiveCauses.stream().anyMatch(t -> t.isInstance(c)))) {
return source.getTrustedRevision(revision, listener);
} else {
return revision;
}
}

@Extension public static class DescriptorImpl extends FlowDefinitionDescriptor {

@NonNull
Expand All @@ -165,22 +193,4 @@ public SCMBinder(String scriptPath) {

}

// TODO seems there is no general-purpose ConsoleNote which simply wraps markup in specified HTML
@SuppressWarnings("rawtypes")
public static class WarningNote extends ConsoleNote {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Restricted(NoExternalUse.class)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see why; it was not used elsewhere to begin with.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm, misread the diff since I didn't expect removal here.

Copy link
Member

@daniel-beck daniel-beck Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, removing this class might cause trouble with old build logs containing non-deserializable annotations? Or is this going somewhere else? Or have you tested it and it'll just silently drop them in build logs nobody cares about anyway given the early abort?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/jenkinsci/jenkins/blob/aa83562568b44aa85ff17bdfab831bbe39c2fd15/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java#L112-L125 should just drop the annotation, i.e. make the text be plain rather than yellow. Not specifically tested.


@Override public ConsoleAnnotator annotate(Object context, MarkupText text, int charPos) {
text.addMarkup(0, text.length(), "<span class='warning-inline'>", "</span>");
return null;
}

@Extension public static final class DescriptorImpl extends ConsoleAnnotationDescriptor {
@NonNull
@Override public String getDisplayName() {
return "Multibranch warnings";
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import com.cloudbees.hudson.plugins.folder.computed.DefaultOrphanedItemStrategy;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Util;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Item;
import hudson.model.Result;
import hudson.model.TaskListener;
Expand Down Expand Up @@ -235,9 +237,11 @@ public static void assertRevisionAction(WorkflowRun build) {
assertEquals(1, mp.getItems().size());
}

@Issue("JENKINS-46795")
@Test public void untrustedRevisions() throws Exception {
sampleGitRepo.init();
sampleGitRepo.write("Jenkinsfile", "node {checkout scm; echo readFile('file')}");
String masterJenkinsfile = "node {checkout scm; echo readFile('file')}";
sampleGitRepo.write("Jenkinsfile", masterJenkinsfile);
sampleGitRepo.write("file", "initial content");
sampleGitRepo.git("add", "Jenkinsfile");
sampleGitRepo.git("commit", "--all", "--message=flow");
Expand All @@ -254,18 +258,54 @@ public static void assertRevisionAction(WorkflowRun build) {
String branch = "some-other-branch-from-Norway";
sampleGitRepo.git("checkout", "-b", branch);
sampleGitRepo.write("Jenkinsfile", "error 'ALL YOUR BUILD STEPS ARE BELONG TO US'");
sampleGitRepo.write("file", "subsequent content");
sampleGitRepo.git("commit", "--all", "--message=big evil laugh");
p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch);
r.waitUntilNoActivity();
b = p.getLastBuild();
assertNotNull(b);
assertEquals(1, b.getNumber());
assertRevisionAction(b);
r.assertBuildStatusSuccess(b);
r.assertBuildStatus(Result.NOT_BUILT, b);
r.assertLogContains(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis("Jenkinsfile"), b);
r.assertLogContains("not trusting", b);
SCMBinder.IGNORE_UNTRUSTED_EDITS = true;
try {
sampleGitRepo.write("file", "subsequent content");
sampleGitRepo.git("commit", "--all", "--message=edits");
p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch);
r.waitUntilNoActivity();
b = p.getLastBuild();
assertNotNull(b);
assertEquals(2, b.getNumber());
r.assertLogContains("subsequent content", b);
r.assertLogContains("not trusting", b);
} finally {
SCMBinder.IGNORE_UNTRUSTED_EDITS = false;
}
sampleGitRepo.write("Jenkinsfile", masterJenkinsfile);
sampleGitRepo.git("commit", "--all", "--message=meekly submitting");
p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch);
r.waitUntilNoActivity();
b = p.getLastBuild();
assertNotNull(b);
assertEquals(3, b.getNumber());
r.assertLogContains("subsequent content", b);
r.assertLogContains("not trusting", b);
sampleGitRepo.write("Jenkinsfile", "node {checkout scm; echo readTrusted('file').toUpperCase()}");
sampleGitRepo.git("commit", "--all", "--message=changes to be approved");
p = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, branch);
r.waitUntilNoActivity();
b = p.getLastBuild();
assertNotNull(b);
assertEquals(4, b.getNumber());
r.assertBuildStatus(Result.NOT_BUILT, b);
r.assertLogContains(Messages.ReadTrustedStep__has_been_modified_in_an_untrusted_revis("Jenkinsfile"), b);
r.assertLogContains("not trusting", b);
b = p.scheduleBuild2(0, new CauseAction(new Cause.UserIdCause())).get();
assertEquals(5, b.getNumber());
r.assertBuildStatusSuccess(b);
r.assertLogContains("SUBSEQUENT CONTENT", b);
r.assertLogNotContains("not trusting", b);
}
public static class WarySource extends GitSCMSource {
public WarySource(String id, String remote, String credentialsId, String includes, String excludes, boolean ignoreOnPushNotifications) {
Expand Down