Skip to content

Commit 6bd4e8b

Browse files
yaroslavafenkindwnusbaum
authored andcommitted
[SECURITY-2450]
1 parent 434009a commit 6bd4e8b

File tree

5 files changed

+211
-5
lines changed

5 files changed

+211
-5
lines changed

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
<dependency>
102102
<groupId>org.jenkins-ci.plugins</groupId>
103103
<artifactId>script-security</artifactId>
104+
<version>1172.v35f6a_0b_8207e</version> <!-- TODO: Remove once this version is included in BOM. -->
104105
</dependency>
105106
<dependency>
106107
<groupId>org.jenkins-ci.plugins</groupId>
@@ -148,6 +149,7 @@
148149
<groupId>org.jenkins-ci.plugins.workflow</groupId>
149150
<artifactId>workflow-job</artifactId>
150151
<scope>test</scope>
152+
<version>1181.va_25d15548158</version> <!-- TODO: Remove once this version is included in BOM. -->
151153
</dependency>
152154
<dependency>
153155
<groupId>org.jenkins-ci.plugins.workflow</groupId>

src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
package org.jenkinsci.plugins.workflow.cps;
2626

27+
import edu.umd.cs.findbugs.annotations.NonNull;
2728
import hudson.Extension;
2829
import hudson.model.Action;
2930
import hudson.model.Item;
@@ -33,6 +34,8 @@
3334
import hudson.model.TaskListener;
3435
import hudson.util.FormValidation;
3536
import hudson.util.StreamTaskListener;
37+
import net.sf.json.JSONObject;
38+
import org.apache.commons.lang.StringUtils;
3639
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
3740
import org.jenkinsci.plugins.workflow.flow.DurabilityHintProvider;
3841
import org.jenkinsci.plugins.workflow.flow.FlowDefinition;
@@ -80,7 +83,8 @@ public CpsFlowDefinition(String script) {
8083
@DataBoundConstructor
8184
public CpsFlowDefinition(String script, boolean sandbox) {
8285
StaplerRequest req = Stapler.getCurrentRequest();
83-
this.script = sandbox ? script : ScriptApproval.get().configuring(script, GroovyLanguage.get(), ApprovalContext.create().withCurrentUser().withItemAsKey(req != null ? req.findAncestorObject(Item.class) : null));
86+
this.script = sandbox ? script : ScriptApproval.get().configuring(script, GroovyLanguage.get(),
87+
ApprovalContext.create().withCurrentUser().withItemAsKey(req != null ? req.findAncestorObject(Item.class) : null), req == null);
8488
this.sandbox = sandbox;
8589
}
8690

@@ -123,14 +127,41 @@ public CpsFlowExecution create(FlowExecutionOwner owner, TaskListener listener,
123127
@Extension
124128
public static class DescriptorImpl extends FlowDefinitionDescriptor {
125129

130+
/* In order to fix SECURITY-2450 without causing significant UX regressions, we decided to continue to
131+
* automatically approve scripts on save if the script was modified by an administrator. To make this possible,
132+
* we added a new hidden input field to the config.jelly to track the pre-save version of the script. Since
133+
* CpsFlowDefinition calls ScriptApproval.configuring in its @DataBoundConstructor, the normal way to handle
134+
* things would be to add an oldScript parameter to the constructor and perform the relevant logic there.
135+
*
136+
* However, that would have compatibility implications for tools like JobDSL, since @DataBoundConstructor
137+
* parameters are required. We cannot use a @DataBoundSetter with a corresponding field and getter to trivially
138+
* make oldScript optional, because we would need to call ScriptApproval.configuring after all
139+
* @DataBoundSetters have been invoked (rather than in the @DataBoundConstructor), which is why we use Descriptor.newInstance.
140+
*/
141+
@Override
142+
public FlowDefinition newInstance(@NonNull StaplerRequest req, @NonNull JSONObject formData) throws FormException {
143+
CpsFlowDefinition cpsFlowDefinition = (CpsFlowDefinition) super.newInstance(req, formData);
144+
if (!cpsFlowDefinition.sandbox && formData.get("oldScript") != null) {
145+
String oldScript = formData.getString("oldScript");
146+
boolean approveIfAdmin = !StringUtils.equals(oldScript, cpsFlowDefinition.script);
147+
if (approveIfAdmin) {
148+
ScriptApproval.get().configuring(cpsFlowDefinition.script, GroovyLanguage.get(),
149+
ApprovalContext.create().withCurrentUser().withItemAsKey(req.findAncestorObject(Item.class)), true);
150+
}
151+
}
152+
return cpsFlowDefinition;
153+
}
154+
126155
@Override
127156
public String getDisplayName() {
128157
return "Pipeline script";
129158
}
130159

131160
@RequirePOST
132-
public FormValidation doCheckScript(@QueryParameter String value, @QueryParameter boolean sandbox) {
133-
return sandbox ? FormValidation.ok() : ScriptApproval.get().checking(value, GroovyLanguage.get());
161+
public FormValidation doCheckScript(@QueryParameter String value, @QueryParameter String oldScript,
162+
@QueryParameter boolean sandbox) {
163+
return sandbox ? FormValidation.ok() :
164+
ScriptApproval.get().checking(value, GroovyLanguage.get(), !StringUtils.equals(oldScript, value));
134165
}
135166

136167
@RequirePOST

src/main/resources/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition/config.jelly

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
-->
2525

2626
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler" xmlns:wfe="/org/jenkinsci/plugins/workflow/editor">
27+
<input type="hidden" name="oldScript" value="${instance.script}"/>
2728
<f:entry title="${%Script}" field="script">
2829
<wfe:workflow-editor />
2930
</f:entry>

src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition2Test.java

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,29 @@
2525
package org.jenkinsci.plugins.workflow.cps;
2626

2727
import com.cloudbees.groovy.cps.CpsTransformer;
28+
import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
29+
import com.gargoylesoftware.htmlunit.html.HtmlForm;
30+
import com.gargoylesoftware.htmlunit.html.HtmlInput;
31+
import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
2832
import hudson.Functions;
2933
import hudson.model.Computer;
3034
import hudson.model.Describable;
3135
import hudson.model.Executor;
36+
import hudson.model.Item;
3237
import hudson.model.Result;
3338
import java.io.Serializable;
3439
import java.util.Collections;
40+
import java.util.List;
3541
import java.util.Set;
3642

3743
import java.util.logging.Level;
44+
45+
import hudson.security.Permission;
3846
import jenkins.model.Jenkins;
3947

4048
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
49+
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
50+
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage;
4151
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
4252
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
4353
import org.jenkinsci.plugins.workflow.steps.Step;
@@ -58,13 +68,16 @@
5868
import org.jvnet.hudson.test.Issue;
5969
import org.jvnet.hudson.test.JenkinsRule;
6070
import org.jvnet.hudson.test.LoggerRule;
71+
import org.jvnet.hudson.test.MockAuthorizationStrategy;
6172
import org.jvnet.hudson.test.TestExtension;
6273
import org.kohsuke.stapler.DataBoundConstructor;
6374

6475
import static org.hamcrest.MatcherAssert.assertThat;
6576
import static org.hamcrest.Matchers.instanceOf;
6677
import static org.junit.Assert.assertEquals;
78+
import static org.junit.Assert.assertFalse;
6779
import static org.junit.Assert.assertNull;
80+
import static org.junit.Assert.assertTrue;
6881
import static org.junit.Assert.fail;
6982

7083
public class CpsFlowDefinition2Test {
@@ -89,7 +102,7 @@ public void endlessRecursion() throws Exception {
89102
WorkflowRun r = jenkins.assertBuildStatus(Result.FAILURE, job.scheduleBuild2(0).get());
90103
jenkins.assertLogContains("look for unbounded recursion", r);
91104

92-
Assert.assertTrue("No queued FlyWeightTask for job should remain after failure", jenkins.jenkins.getQueue().isEmpty());
105+
assertTrue("No queued FlyWeightTask for job should remain after failure", jenkins.jenkins.getQueue().isEmpty());
93106

94107
for (Computer c : jenkins.jenkins.getComputers()) {
95108
for (Executor ex : c.getExecutors()) {
@@ -117,7 +130,7 @@ public void endlessRecursionNonCPS() throws Exception {
117130
// Should have failed with error about excessive recursion depth
118131
WorkflowRun r = jenkins.assertBuildStatus(Result.FAILURE, job.scheduleBuild2(0).get());
119132

120-
Assert.assertTrue("No queued FlyWeightTask for job should remain after failure", jenkins.jenkins.getQueue().isEmpty());
133+
assertTrue("No queued FlyWeightTask for job should remain after failure", jenkins.jenkins.getQueue().isEmpty());
121134

122135
for (Computer c : jenkins.jenkins.getComputers()) {
123136
for (Executor ex : c.getExecutors()) {
@@ -852,6 +865,141 @@ public void scriptInitializerCallsCpsTransformedMethod() throws Exception {
852865
assertNull(Jenkins.get().getDescription());
853866
}
854867

868+
@Issue("SECURITY-2450")
869+
@Test
870+
public void cpsScriptNonAdminConfiguration() throws Exception {
871+
jenkins.jenkins.setSecurityRealm(jenkins.createDummySecurityRealm());
872+
873+
MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy();
874+
mockStrategy.grant(Jenkins.READ).everywhere().to("devel");
875+
for (Permission p : Item.PERMISSIONS.getPermissions()) {
876+
mockStrategy.grant(p).everywhere().to("devel");
877+
}
878+
jenkins.jenkins.setAuthorizationStrategy(mockStrategy);
879+
880+
JenkinsRule.WebClient wcDevel = jenkins.createWebClient();
881+
wcDevel.login("devel");
882+
883+
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
884+
885+
HtmlForm config = wcDevel.getPage(p, "configure").getFormByName("config");
886+
List<HtmlTextArea> scripts = config.getTextAreasByName("_.script");
887+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
888+
HtmlTextArea script = scripts.get(scripts.size() - 1);
889+
String groovy = "echo 'hi from cpsScriptNonAdminConfiguration'";
890+
script.setText(groovy);
891+
892+
List<HtmlInput> sandboxes = config.getInputsByName("_.sandbox");
893+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
894+
HtmlCheckBoxInput sandbox = (HtmlCheckBoxInput) sandboxes.get(sandboxes.size() - 1);
895+
assertTrue(sandbox.isChecked());
896+
sandbox.setChecked(false);
897+
898+
jenkins.submit(config);
899+
900+
assertEquals(1, ScriptApproval.get().getPendingScripts().size());
901+
assertFalse(ScriptApproval.get().isScriptApproved(groovy, GroovyLanguage.get()));
902+
}
903+
904+
@Issue("SECURITY-2450")
905+
@Test
906+
public void cpsScriptAdminConfiguration() throws Exception {
907+
jenkins.jenkins.setSecurityRealm(jenkins.createDummySecurityRealm());
908+
909+
MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy();
910+
mockStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");
911+
for (Permission p : Item.PERMISSIONS.getPermissions()) {
912+
mockStrategy.grant(p).everywhere().to("admin");
913+
}
914+
jenkins.jenkins.setAuthorizationStrategy(mockStrategy);
915+
916+
JenkinsRule.WebClient admin = jenkins.createWebClient();
917+
admin.login("admin");
918+
919+
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
920+
921+
HtmlForm config = admin.getPage(p, "configure").getFormByName("config");
922+
List<HtmlTextArea> scripts = config.getTextAreasByName("_.script");
923+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
924+
HtmlTextArea script = scripts.get(scripts.size() - 1);
925+
String groovy = "echo 'hi from cpsScriptAdminConfiguration'";
926+
script.setText(groovy);
927+
928+
List<HtmlInput> sandboxes = config.getInputsByName("_.sandbox");
929+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
930+
HtmlCheckBoxInput sandbox = (HtmlCheckBoxInput) sandboxes.get(sandboxes.size() - 1);
931+
assertTrue(sandbox.isChecked());
932+
sandbox.setChecked(false);
933+
934+
jenkins.submit(config);
935+
936+
assertTrue(ScriptApproval.get().isScriptApproved(groovy, GroovyLanguage.get()));
937+
}
938+
939+
@Issue("SECURITY-2450")
940+
@Test
941+
public void cpsScriptAdminModification() throws Exception {
942+
jenkins.jenkins.setSecurityRealm(jenkins.createDummySecurityRealm());
943+
944+
MockAuthorizationStrategy mockStrategy = new MockAuthorizationStrategy();
945+
mockStrategy.grant(Jenkins.READ).everywhere().to("devel");
946+
mockStrategy.grant(Jenkins.ADMINISTER).everywhere().to("admin");
947+
for (Permission p : Item.PERMISSIONS.getPermissions()) {
948+
mockStrategy.grant(p).everywhere().to("devel");
949+
mockStrategy.grant(p).everywhere().to("admin");
950+
}
951+
jenkins.jenkins.setAuthorizationStrategy(mockStrategy);
952+
953+
JenkinsRule.WebClient wc = jenkins.createWebClient();
954+
wc.login("devel");
955+
956+
WorkflowJob p = jenkins.createProject(WorkflowJob.class);
957+
String userGroovy = "echo 'hi from devel'";
958+
String adminGroovy = "echo 'hi from admin'";
959+
960+
// initial configuration by user, script ends up in pending
961+
{
962+
HtmlForm config = wc.getPage(p, "configure").getFormByName("config");
963+
List<HtmlTextArea> scripts = config.getTextAreasByName("_.script");
964+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
965+
HtmlTextArea script = scripts.get(scripts.size() - 1);
966+
script.setText(userGroovy);
967+
968+
List<HtmlInput> sandboxes = config.getInputsByName("_.sandbox");
969+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
970+
HtmlCheckBoxInput sandbox = (HtmlCheckBoxInput) sandboxes.get(sandboxes.size() - 1);
971+
assertTrue(sandbox.isChecked());
972+
sandbox.setChecked(false);
973+
974+
jenkins.submit(config);
975+
976+
assertFalse(ScriptApproval.get().isScriptApproved(userGroovy, GroovyLanguage.get()));
977+
}
978+
979+
wc.login("admin");
980+
981+
// modification by admin, script gets approved automatically
982+
{
983+
HtmlForm config = wc.getPage(p, "configure").getFormByName("config");
984+
List<HtmlTextArea> scripts = config.getTextAreasByName("_.script");
985+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
986+
HtmlTextArea script = scripts.get(scripts.size() - 1);
987+
script.setText(adminGroovy);
988+
989+
List<HtmlInput> sandboxes = config.getInputsByName("_.sandbox");
990+
// Get the last one, because previous ones might be from Lockable Resources during PCT.
991+
HtmlCheckBoxInput sandbox = (HtmlCheckBoxInput) sandboxes.get(sandboxes.size() - 1);
992+
assertFalse(sandbox.isChecked());
993+
994+
jenkins.submit(config);
995+
996+
// script content was modified by admin, so it should be approved upon save
997+
// the one that had been submitted by the user previously stays in pending
998+
assertTrue(ScriptApproval.get().isScriptApproved(adminGroovy, GroovyLanguage.get()));
999+
assertFalse(ScriptApproval.get().isScriptApproved(userGroovy, GroovyLanguage.get()));
1000+
}
1001+
}
1002+
8551003
public static class UnsafeParameterStep extends Step implements Serializable {
8561004
private final UnsafeDescribable val;
8571005
@DataBoundConstructor
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.jenkinsci.plugins.workflow.cps;
2+
3+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
4+
import org.junit.Rule;
5+
import org.junit.Test;
6+
import org.jvnet.hudson.test.JenkinsRule;
7+
import org.jvnet.hudson.test.RealJenkinsRule;
8+
9+
public class CpsFlowDefinitionRJRTest {
10+
11+
@Rule
12+
public RealJenkinsRule rjr = new RealJenkinsRule();
13+
14+
@Test
15+
public void smokes() throws Throwable {
16+
rjr.then(CpsFlowDefinitionRJRTest::doesItSmoke);
17+
}
18+
19+
private static void doesItSmoke(JenkinsRule r) throws Exception {
20+
WorkflowJob p = r.createProject(WorkflowJob.class, "p");
21+
p.setDefinition(new CpsFlowDefinition("print Jenkins.get().getRootDir().toString()", false));
22+
r.assertBuildStatusSuccess(p.scheduleBuild2(0));
23+
}
24+
}

0 commit comments

Comments
 (0)