Skip to content

Commit

Permalink
SECURITY-2033
Browse files Browse the repository at this point in the history
  • Loading branch information
Pldi23 committed Jan 10, 2022
2 parents 9fa69fd + a596f65 commit 467ed6c
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 3 deletions.
Expand Up @@ -35,9 +35,11 @@
import hudson.model.Queue;
import hudson.model.queue.Tasks;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.authentication.tokens.api.AuthenticationTokens;
import jenkins.model.Jenkins;
import jenkins.scm.api.SCMSourceOwner;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AncestorInPath;
Expand Down Expand Up @@ -80,6 +82,10 @@ static ListBoxModel fillCredentialsIdItems(
@QueryParameter String serverUrl) {
StandardListBoxModel result = new StandardListBoxModel();
result.includeEmptyValue();
AccessControlled contextToCheck = context == null ? Jenkins.get() : context;
if (!contextToCheck.hasPermission(CredentialsProvider.VIEW)) {
return result;
}
result.includeMatchingAs(
context instanceof Queue.Task
? Tasks.getDefaultAuthenticationOf((Queue.Task) context)
Expand All @@ -97,6 +103,8 @@ static FormValidation checkCredentialsId(
@QueryParameter String value,
@QueryParameter String serverUrl) {
if (!value.isEmpty()) {
AccessControlled contextToCheck = context == null ? Jenkins.get() : context;
contextToCheck.checkPermission(CredentialsProvider.VIEW);
if (CredentialsMatchers.firstOrNull(
CredentialsProvider.lookupCredentials(
StandardCertificateCredentials.class,
Expand Down
Expand Up @@ -43,8 +43,10 @@
import hudson.Util;
import hudson.console.HyperlinkNote;
import hudson.model.Action;
import hudson.model.Item;
import hudson.model.TaskListener;
import hudson.plugins.git.GitSCM;
import hudson.security.AccessControlled;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import java.io.IOException;
Expand Down Expand Up @@ -607,7 +609,11 @@ public boolean isServerUrlSelectable() {
}

@SuppressWarnings("unused") // used By stapler
public ListBoxModel doFillServerUrlItems() {
public ListBoxModel doFillServerUrlItems(@AncestorInPath SCMSourceOwner context) {
AccessControlled contextToCheck = context == null ? Jenkins.get() : context;
if (!contextToCheck.hasPermission(Item.CONFIGURE)) {
return new ListBoxModel();
}
return BitbucketEndpointConfiguration.get().getEndpointItems();
}

Expand Down
Expand Up @@ -61,6 +61,7 @@
import hudson.model.TaskListener;
import hudson.plugins.git.GitSCM;
import hudson.scm.SCM;
import hudson.security.AccessControlled;
import hudson.util.FormFillFailure;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
Expand Down Expand Up @@ -122,6 +123,7 @@
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;

/**
* SCM source implementation for Bitbucket.
Expand Down Expand Up @@ -1203,7 +1205,9 @@ public FormValidation doCheckCredentialsId(@CheckForNull @AncestorInPath SCMSour
}

@SuppressWarnings("unused") // used By stapler
public static FormValidation doCheckServerUrl(@QueryParameter String value) {
public static FormValidation doCheckServerUrl(@AncestorInPath SCMSourceOwner context, @QueryParameter String value) {
AccessControlled contextToCheck = context == null ? Jenkins.get() : context;
contextToCheck.checkPermission(Item.CONFIGURE);
if (BitbucketEndpointConfiguration.get().findEndpoint(value) == null) {
return FormValidation.error("Unregistered Server: " + value);
}
Expand All @@ -1216,7 +1220,11 @@ public boolean isServerUrlSelectable() {
}

@SuppressWarnings("unused") // used By stapler
public ListBoxModel doFillServerUrlItems() {
public ListBoxModel doFillServerUrlItems(@AncestorInPath SCMSourceOwner context) {
AccessControlled contextToCheck = context == null ? Jenkins.get() : context;
if (!contextToCheck.hasPermission(Item.CONFIGURE)) {
return new ListBoxModel();
}
return BitbucketEndpointConfiguration.get().getEndpointItems();
}

Expand All @@ -1226,6 +1234,7 @@ public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner cont
}

@SuppressWarnings("unused") // used By stapler
@RequirePOST
public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context,
@QueryParameter String serverUrl,
@QueryParameter String credentialsId,
Expand Down
Expand Up @@ -31,9 +31,11 @@
import hudson.Extension;
import hudson.util.FormValidation;
import java.util.List;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.verb.POST;

/**
* Represents <a href="https://bitbucket.org">Bitbucket Cloud</a>.
Expand Down Expand Up @@ -152,6 +154,7 @@ public String getDisplayName() {
}

public FormValidation doShowStats() {
Jenkins.get().checkPermission(Jenkins.MANAGE);
List<String> stats = BitbucketCloudApiClient.stats();
StringBuilder builder = new StringBuilder();
for (String stat : stats) {
Expand All @@ -160,7 +163,9 @@ public FormValidation doShowStats() {
return FormValidation.okWithMarkup(builder.toString());
}

@POST
public FormValidation doClear() {
Jenkins.get().checkPermission(Jenkins.MANAGE);
BitbucketCloudApiClient.clearCaches();
return FormValidation.ok("Caches cleared");
}
Expand Down
@@ -0,0 +1,183 @@
package com.cloudbees.jenkins.plugins.bitbucket;

import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import com.gargoylesoftware.htmlunit.Page;
import hudson.model.Item;
import hudson.model.User;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.util.ListBoxModel;
import java.io.IOException;
import java.net.HttpURLConnection;
import jenkins.model.Jenkins;
import org.hamcrest.CoreMatchers;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.fail;

public class Security2033Test {

private static final String PROJECT_NAME = "p";
private static final String NOT_AUTHORIZED_USER = "userNoPermission";
private static final String SERVER_URL = "server.url";

@Rule
public JenkinsRule j = new JenkinsRule();

private WorkflowMultiBranchProject pr;

@Before
public void setup() throws Exception {
pr = j.jenkins.createProject(WorkflowMultiBranchProject.class, PROJECT_NAME);
setUpAuthorization();
initCredentials();
}

@Issue("SECURITY-2033")
@Test
public void doFillCredentialsIdItemsSCMSourceWhenUserWithoutCredentialsViewPermissionThenListNotPopulated() {
BitbucketSCMSource.DescriptorImpl descriptor = (BitbucketSCMSource.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketSCMSource.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
ListBoxModel actual = descriptor.doFillCredentialsIdItems(pr, SERVER_URL);
ListBoxModel expected = new ListBoxModel(new ListBoxModel.Option("- none -", ""));
assertListBoxModel(actual, expected);
}
}

@Issue("SECURITY-2033")
@Test
public void doFillCredentialsIdItemsSCMNavigatorWhenUserWithoutCredentialsViewPermissionThenListNotPopulated() {
BitbucketSCMNavigator.DescriptorImpl descriptor = (BitbucketSCMNavigator.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketSCMNavigator.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
ListBoxModel actual = descriptor.doFillCredentialsIdItems(pr, SERVER_URL);
ListBoxModel expected = new ListBoxModel(new ListBoxModel.Option("- none -", ""));
assertListBoxModel(actual, expected);
}
}

@Issue("SECURITY-2033")
@Test
public void doCheckCredentialsIdSCMNavigatorWhenUserWithoutCredentialsViewPermissionThenReturnForbiddenStatus() {
BitbucketSCMNavigator.DescriptorImpl descriptor = (BitbucketSCMNavigator.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketSCMNavigator.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
descriptor.doCheckCredentialsId(pr, SERVER_URL, "nonEmpty");
fail("Should fail with AccessDeniedException2");
} catch (Exception accessDeniedException2) {
assertThat(accessDeniedException2.getMessage(), is(NOT_AUTHORIZED_USER + " is missing the Credentials/View permission"));
}
}

@Issue("SECURITY-2033")
@Test
public void doCheckCredentialsIdSCMSourceWhenUserWithoutCredentialsViewPermissionThenReturnForbiddenStatus() {
BitbucketSCMSource.DescriptorImpl descriptor = (BitbucketSCMSource.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketSCMSource.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
descriptor.doCheckCredentialsId(pr, SERVER_URL, "nonEmpty");
fail("Should fail with AccessDeniedException2 but not");
} catch (Exception accessDeniedException2) {
assertThat(accessDeniedException2.getMessage(), is(NOT_AUTHORIZED_USER + " is missing the Credentials/View permission"));
}
}

@Issue("SECURITY-2033")
@Test
public void doFillServerUrlItemsSCMNavigatorWhenUserWithoutPermissionThenReturnEmptyList() {
BitbucketSCMNavigator.DescriptorImpl descriptor = (BitbucketSCMNavigator.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketSCMNavigator.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
ListBoxModel actual = descriptor.doFillServerUrlItems(pr);
assertThat(actual, is(empty()));
}
}

@Issue("SECURITY-2033")
@Test
public void doFillServerUrlItemsSCMSourceWhenUserWithoutPermissionThenReturnEmptyList() {
BitbucketSCMSource.DescriptorImpl descriptor = (BitbucketSCMSource.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketSCMSource.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
ListBoxModel actual = descriptor.doFillServerUrlItems(pr);
assertThat(actual, is(empty()));
}
}

@Issue("SECURITY-2033")
@Test
public void doCheckServerUrlWhenUserWithoutPermissionThenReturnForbiddenStatus() {
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
BitbucketSCMSource.DescriptorImpl.doCheckServerUrl(pr, SERVER_URL);
fail("Should fail with AccessDeniedException2");
} catch (Exception accessDeniedException2) {
assertThat(accessDeniedException2.getMessage(), is(NOT_AUTHORIZED_USER + " is missing the Job/Configure permission"));
}
}

@Issue("SECURITY-2033")
@Test
public void doShowStatsWhenUserWithoutAdminPermissionThenReturnForbiddenStatus() {
BitbucketCloudEndpoint.DescriptorImpl descriptor = (BitbucketCloudEndpoint.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketCloudEndpoint.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
descriptor.doShowStats();
fail("Should fail with AccessDeniedException2");
} catch (Exception accessDeniedException2) {
assertThat(accessDeniedException2.getMessage(), is(NOT_AUTHORIZED_USER + " is missing the Overall/Administer permission"));
}
}

@Issue("SECURITY-2033")
@Test
public void doClearWhenUserWithoutAdminPermissionThenReturnForbiddenStatus() {
BitbucketCloudEndpoint.DescriptorImpl descriptor = (BitbucketCloudEndpoint.DescriptorImpl) Jenkins.get().getDescriptorOrDie(BitbucketCloudEndpoint.class);
try (ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(NOT_AUTHORIZED_USER))) {
descriptor.doClear();
fail("Should fail with AccessDeniedException2");
} catch (Exception accessDeniedException2) {
assertThat(accessDeniedException2.getMessage(), is(NOT_AUTHORIZED_USER + " is missing the Overall/Administer permission"));
}
}

@Issue("SECURITY-2033")
@Test
public void doClearWhenInvokedUsingGetMethodThenResourceNotFound() throws Exception {
JenkinsRule.WebClient webClient = j .createWebClient().withThrowExceptionOnFailingStatusCode(false);
webClient.login(NOT_AUTHORIZED_USER);
Page page = webClient.goTo("job/" + PROJECT_NAME +"/descriptorByName/com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint/clear");

assertThat(page.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_NOT_FOUND));
assertThat(page.getWebResponse().getContentAsString(), containsString("Stapler processed this HTTP request as follows, but couldn't find the resource to consume the request"));
}

private void initCredentials() throws IOException {
StandardUsernamePasswordCredentials key = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "id", "desc", "username", "pass");
SystemCredentialsProvider.getInstance().getCredentials().add(key);

SystemCredentialsProvider.getInstance().save();
}

private void setUpAuthorization() {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.READ, Item.READ).everywhere().to(NOT_AUTHORIZED_USER));
}

private static void assertListBoxModel(ListBoxModel actual, ListBoxModel expected) {
assertThat(actual, CoreMatchers.is(not(empty())));
assertThat(actual, hasSize(expected.size()));
assertThat(actual.get(0).name, CoreMatchers.is(expected.get(0).name));
assertThat(actual.get(0).value, CoreMatchers.is(expected.get(0).value));
}
}
@@ -0,0 +1,39 @@
package com.cloudbees.jenkins.plugins.bitbucket;

import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.net.HttpURLConnection;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;

public class Security2467Test {

@Rule
public JenkinsRule j = new JenkinsRule();

@Issue("SECURITY-2467")
@Test
public void doFillRepositoryItemsWhenInvokedUsingGetMethodThenReturnMethodNotAllowed() throws Exception {
String admin = "Admin";
String projectName = "p";
WorkflowMultiBranchProject pr = j.jenkins.createProject(WorkflowMultiBranchProject.class, projectName);
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy().
grant(Jenkins.ADMINISTER).everywhere().to(admin));

JenkinsRule.WebClient webClient = j.createWebClient().withThrowExceptionOnFailingStatusCode(false);
webClient.login(admin);
HtmlPage htmlPage = webClient.goTo("job/" + projectName +"/descriptorByName/com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMSource/fillRepositoryItems?serverUrl=http://hacker:9000&credentialsId=ID_Admin&repoOwner=admin");

assertThat(htmlPage.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_BAD_METHOD));
assertThat(htmlPage.getWebResponse().getContentAsString(), containsString("This URL requires POST"));
}
}

0 comments on commit 467ed6c

Please sign in to comment.