Skip to content

Commit 042de66

Browse files
Kevin-CBjenkinsci-cert-ci
authored andcommitted
1 parent c3d776a commit 042de66

File tree

4 files changed

+305
-7
lines changed

4 files changed

+305
-7
lines changed

core/src/main/java/hudson/Functions.java

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ public class Functions {
203203
private static final AtomicLong iota = new AtomicLong();
204204
private static Logger LOGGER = Logger.getLogger(Functions.class.getName());
205205

206+
/**
207+
* Escape hatch to use the non-recursive f:password masking.
208+
*/
209+
private static /* non-final */ boolean NON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK = SystemProperties.getBoolean(Functions.class.getName() + ".nonRecursivePasswordMaskingPermissionCheck");
210+
211+
206212
public Functions() {
207213
}
208214

@@ -2252,13 +2258,38 @@ public String getPasswordValue(Object o) {
22522258
StaplerRequest2 req = Stapler.getCurrentRequest2();
22532259
if (o instanceof Secret || Secret.BLANK_NONSECRET_PASSWORD_FIELDS_WITHOUT_ITEM_CONFIGURE) {
22542260
if (req != null) {
2255-
Item item = req.findAncestorObject(Item.class);
2256-
if (item != null && !item.hasPermission(Item.CONFIGURE)) {
2257-
return "********";
2258-
}
2259-
Computer computer = req.findAncestorObject(Computer.class);
2260-
if (computer != null && !computer.hasPermission(Computer.CONFIGURE)) {
2261-
return "********";
2261+
if (NON_RECURSIVE_PASSWORD_MASKING_PERMISSION_CHECK) {
2262+
Item item = req.findAncestorObject(Item.class);
2263+
if (item != null && !item.hasPermission(Item.CONFIGURE)) {
2264+
return "********";
2265+
}
2266+
Computer computer = req.findAncestorObject(Computer.class);
2267+
if (computer != null && !computer.hasPermission(Computer.CONFIGURE)) {
2268+
return "********";
2269+
}
2270+
} else {
2271+
List<Ancestor> ancestors = req.getAncestors();
2272+
for (Ancestor ancestor : Iterators.reverse(ancestors)) {
2273+
Object type = ancestor.getObject();
2274+
if (type instanceof Item item) {
2275+
if (!item.hasPermission(Item.CONFIGURE)) {
2276+
return "********";
2277+
}
2278+
break;
2279+
}
2280+
if (type instanceof Computer computer) {
2281+
if (!computer.hasPermission(Computer.CONFIGURE)) {
2282+
return "********";
2283+
}
2284+
break;
2285+
}
2286+
if (type instanceof View view) {
2287+
if (!view.hasPermission(View.CONFIGURE)) {
2288+
return "********";
2289+
}
2290+
break;
2291+
}
2292+
}
22622293
}
22632294
}
22642295
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package jenkins.security;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.not;
6+
7+
import com.cloudbees.hudson.plugins.folder.Folder;
8+
import hudson.model.Action;
9+
import hudson.model.Computer;
10+
import hudson.model.FreeStyleBuild;
11+
import hudson.model.FreeStyleProject;
12+
import hudson.model.Item;
13+
import hudson.model.ListView;
14+
import hudson.model.View;
15+
import hudson.slaves.DumbSlave;
16+
import hudson.util.Secret;
17+
import jenkins.model.Jenkins;
18+
import org.htmlunit.Page;
19+
import org.junit.jupiter.api.BeforeEach;
20+
import org.junit.jupiter.api.Test;
21+
import org.jvnet.hudson.test.Issue;
22+
import org.jvnet.hudson.test.JenkinsRule;
23+
import org.jvnet.hudson.test.MockAuthorizationStrategy;
24+
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
25+
26+
@WithJenkins
27+
class Security1809Test {
28+
29+
private JenkinsRule j;
30+
31+
private final String password = "p4ssw0rd";
32+
33+
private final Secret secretPassword = Secret.fromString(password);
34+
35+
@BeforeEach
36+
void setUp(JenkinsRule rule) {
37+
j = rule;
38+
}
39+
40+
@Test
41+
@Issue("SECURITY-1809")
42+
void passwordIsMaskedForView() throws Exception {
43+
final PasswordView view = new PasswordView("view1", secretPassword);
44+
j.jenkins.addView(view);
45+
46+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
47+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
48+
.grant(Jenkins.READ, View.READ).everywhere().to("readUser")
49+
.grant(Jenkins.READ, View.READ, View.CONFIGURE).everywhere().to("configureUser"));
50+
51+
String url = view.getUrl() + "password";
52+
53+
// configure permission allow to see encrypted value
54+
assertContainsOnlyEncryptedSecret("configureUser", url);
55+
56+
// read permission get only redacted value
57+
assertContainsOnlyMaskedSecret("readUser", url);
58+
}
59+
60+
@Test
61+
@Issue("SECURITY-1809")
62+
void passwordIsMaskedForPrimaryView() throws Exception {
63+
final PasswordView view = new PasswordView("view1", secretPassword);
64+
j.jenkins.addView(view);
65+
j.jenkins.setPrimaryView(view);
66+
67+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
68+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
69+
.grant(Jenkins.READ).everywhere().to("readUser")
70+
.grant(Jenkins.READ, View.READ, View.CONFIGURE).everywhere().to("configureUser"));
71+
72+
String url = "password";
73+
74+
// configure permission allow to see encrypted value
75+
assertContainsOnlyEncryptedSecret("configureUser", url);
76+
77+
// read permission get only redacted value
78+
assertContainsOnlyMaskedSecret("readUser", url);
79+
}
80+
81+
@Test
82+
void passwordIsMaskedForAgent() throws Exception {
83+
final DumbSlave agent = j.createSlave("agent1", "", null);
84+
85+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
86+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
87+
.grant(Jenkins.READ, Computer.CONFIGURE).everywhere().to("configureUser")
88+
.grant(Jenkins.READ).everywhere().to("readUser"));
89+
90+
agent.toComputer().addAction(new PasswordAction(secretPassword));
91+
92+
String url = agent.toComputer().getUrl() + "password";
93+
94+
// configure permission allow to see encrypted value
95+
assertContainsOnlyEncryptedSecret("configureUser", url);
96+
97+
// read permission get only redacted value
98+
assertContainsOnlyMaskedSecret("readUser", url);
99+
}
100+
101+
@Test
102+
void passwordIsMaskedForJob() throws Exception {
103+
final FreeStyleProject job = j.createFreeStyleProject();
104+
FreeStyleBuild build = j.buildAndAssertSuccess(job);
105+
build.addAction(new PasswordAction(secretPassword));
106+
107+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
108+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
109+
.grant(Jenkins.READ, Item.READ, Item.CONFIGURE).everywhere().to("configureUser")
110+
.grant(Jenkins.READ, Item.READ).everywhere().to("readUser"));
111+
112+
String url = build.getUrl() + "password";
113+
114+
// configure permission allow to see encrypted value
115+
assertContainsOnlyEncryptedSecret("configureUser", url);
116+
117+
// read permission get only redacted value
118+
assertContainsOnlyMaskedSecret("readUser", url);
119+
}
120+
121+
@Test
122+
void permissionIsCheckedOnClosestAncestor() throws Exception {
123+
final PasswordView view = new PasswordView("view1", secretPassword);
124+
j.jenkins.addView(view);
125+
126+
final FreeStyleProject job = j.createFreeStyleProject("job1");
127+
FreeStyleBuild build = j.buildAndAssertSuccess(job);
128+
build.addAction(new ActionWithView(view));
129+
130+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
131+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
132+
.grant(Jenkins.READ, Item.READ, View.READ, Item.CONFIGURE).everywhere().to("itemConfigureUser")
133+
.grant(Jenkins.READ, Item.READ, View.READ, View.CONFIGURE).everywhere().to("viewConfigureUser"));
134+
135+
String url = build.getUrl() + "myAction/view/password";
136+
137+
// View/Configure permission allow to see encrypted value
138+
assertContainsOnlyEncryptedSecret("viewConfigureUser", url);
139+
140+
// Item/Configure permission get only redacted value
141+
assertContainsOnlyMaskedSecret("itemConfigureUser", url);
142+
}
143+
144+
@Test
145+
@Issue("SECURITY-1809")
146+
void permissionIsCorrectlyCheckedOnNestedObject() throws Exception {
147+
final Folder folder = j.jenkins.createProject(Folder.class, "folder1");
148+
final FreeStyleProject job = folder.createProject(FreeStyleProject.class, "job1");
149+
FreeStyleBuild build = j.buildAndAssertSuccess(job);
150+
build.addAction(new PasswordAction(secretPassword));
151+
152+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
153+
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
154+
// Item.CONFIGURE on job1 but NOT on folder1
155+
.grant(Jenkins.READ, Item.READ).everywhere().to("jobConfigureUser")
156+
.grant(Item.CONFIGURE).onItems(job).to("jobConfigureUser")
157+
// Item.CONFIGURE on folder1 but NOT on job1
158+
.grant(Jenkins.READ, Item.READ).everywhere().to("folderConfigureUser")
159+
.grant(Item.CONFIGURE).onItems(folder).to("folderConfigureUser"));
160+
161+
String url = build.getUrl() + "password";
162+
163+
// Item/Configure permission on job1 allow to see encrypted value
164+
assertContainsOnlyEncryptedSecret("jobConfigureUser", url);
165+
166+
// Item/Configure permission only on folder1 get only redacted value
167+
assertContainsOnlyMaskedSecret("folderConfigureUser", url);
168+
}
169+
170+
private void assertContainsOnlyEncryptedSecret(String user, String url) throws Exception {
171+
try (JenkinsRule.WebClient wc = j.createWebClient().login(user)) {
172+
Page page = wc.goTo(url);
173+
String content = page.getWebResponse().getContentAsString();
174+
175+
assertThat(content, not(containsString(password)));
176+
assertThat(content, containsString(secretPassword.getEncryptedValue()));
177+
}
178+
}
179+
180+
private void assertContainsOnlyMaskedSecret(String user, String url) throws Exception {
181+
try (JenkinsRule.WebClient wc = j.createWebClient().login(user)) {
182+
Page page = wc.goTo(url);
183+
String content = page.getWebResponse().getContentAsString();
184+
185+
assertThat(content, containsString("********"));
186+
assertThat(content, not(containsString(password)));
187+
assertThat(content, not(containsString(secretPassword.getEncryptedValue())));
188+
}
189+
}
190+
191+
public static class PasswordView extends ListView {
192+
private final Secret secret;
193+
194+
PasswordView(String name, Secret secret) {
195+
super(name);
196+
this.secret = secret;
197+
}
198+
199+
public Secret getSecret() {
200+
return secret;
201+
}
202+
}
203+
204+
public static class PasswordAction implements Action {
205+
private final Secret secret;
206+
207+
PasswordAction(Secret secret) {
208+
this.secret = secret;
209+
}
210+
211+
public Secret getSecret() {
212+
return secret;
213+
}
214+
215+
@Override
216+
public String getIconFileName() { return null; }
217+
218+
@Override
219+
public String getDisplayName() { return null; }
220+
221+
@Override
222+
public String getUrlName() { return "password"; }
223+
}
224+
225+
public static class ActionWithView implements Action {
226+
private final PasswordView view;
227+
228+
ActionWithView(PasswordView view) {
229+
this.view = view;
230+
}
231+
232+
public PasswordView getView() {
233+
return view;
234+
}
235+
236+
@Override
237+
public String getIconFileName() { return null; }
238+
239+
@Override
240+
public String getDisplayName() { return null; }
241+
242+
@Override
243+
public String getUrlName() { return "myAction"; }
244+
}
245+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
3+
<l:layout title="Test Password">
4+
<l:main-panel>
5+
<j:set var="instance" value="${it}" />
6+
<f:entry title="Password" field="secret">
7+
<f:password />
8+
</f:entry>
9+
</l:main-panel>
10+
</l:layout>
11+
</j:jelly>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:l="/lib/layout">
3+
<l:layout title="Test Password - View">
4+
<l:main-panel>
5+
<j:set var="instance" value="${it}" />
6+
<f:entry title="Password" field="secret">
7+
<f:password />
8+
</f:entry>
9+
</l:main-panel>
10+
</l:layout>
11+
</j:jelly>

0 commit comments

Comments
 (0)