CVE-2022-20618
Description
Jenkins Bitbucket Branch Source Plugin 737.vdf9dc06105be and earlier has a missing permission check that allows attackers with Overall/Read access to enumerate credential IDs.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Bitbucket Branch Source Plugin 737.vdf9dc06105be and earlier has a missing permission check that allows attackers with Overall/Read access to enumerate credential IDs.
Vulnerability
The Bitbucket Branch Source Plugin for Jenkins, in versions 737.vdf9dc06105be and earlier, lacks a permission check in a method that exposes credential IDs. This allows any attacker with Overall/Read access (the minimal default access level) to enumerate the IDs of credentials stored in Jenkins [1][4]. The affected plugin does not require the proper Credentials/View permission to list credential IDs, leaking them via a form fill method or similar endpoint.
Exploitation
An attacker only needs Overall/Read access, which is granted by default to any authenticated user in many Jenkins configurations [1]. With this minimal privilege, the attacker can craft a request to the vulnerable endpoint (e.g., doFillCredentialsIdItems) and receive a list of credential IDs. No additional authentication, user interaction, or special network position is required beyond being able to reach the Jenkins instance [1][3].
Impact
Successful exploitation reveals credential IDs (but not the secret values) of all credentials stored in Jenkins. While the actual credential secrets are not exposed, knowledge of credential IDs can assist in further attacks, such as identifying which credentials to target for misuse or exploiting other vulnerabilities that require knowing a credential ID [1][4]. The integrity and availability of the system are not directly affected.
Mitigation
The vulnerability is fixed in Bitbucket Branch Source Plugin 746.v350d2781c184 and later [2]. Users should upgrade to at least this version, which was released on 2022-01-12 [2]. No workaround is available for versions prior to the fix; the only mitigation is to upgrade the plugin. The plugin is not known to be listed in CISA's KEV catalog.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jenkins-ci.plugins:cloudbees-bitbucket-branch-sourceMaven | >= 726.v7e6f53de133c, < 746.v350d2781c184 | 746.v350d2781c184 |
org.jenkins-ci.plugins:cloudbees-bitbucket-branch-sourceMaven | >= 720.vbe985dd73d66, < 725.vd9f8be0fa250 | 725.vd9f8be0fa250 |
org.jenkins-ci.plugins:cloudbees-bitbucket-branch-sourceMaven | >= 2.9.8, < 2.9.11.2 | 2.9.11.2 |
org.jenkins-ci.plugins:cloudbees-bitbucket-branch-sourceMaven | < 2.9.7.2 | 2.9.7.2 |
Affected products
2- ghsa-coordsRange: >= 726.v7e6f53de133c, < 746.v350d2781c184
- Range: unspecified
Patches
1467ed6c94af8SECURITY-2033
6 files changed · +253 −3
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketCredentials.java+8 −0 modified@@ -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; @@ -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) @@ -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,
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java+7 −1 modified@@ -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; @@ -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(); }
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java+11 −2 modified@@ -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; @@ -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. @@ -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); } @@ -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(); } @@ -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,
src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/BitbucketCloudEndpoint.java+5 −0 modified@@ -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>. @@ -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) { @@ -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"); }
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/Security2033Test.java+183 −0 added@@ -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)); + } +}
src/test/java/com/cloudbees/jenkins/plugins/bitbucket/Security2467Test.java+39 −0 added@@ -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")); + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-w2mh-6xj5-f77fghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-20618ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/01/12/6ghsamailing-listx_refsource_MLISTWEB
- github.com/CVEProject/cvelist/blob/2d78eb36f4d084db7fb35f1535d8d84fdcb7d859/2022/20xxx/CVE-2022-20618.jsonghsaWEB
- github.com/jenkinsci/bitbucket-branch-source-plugin/commit/467ed6c94af8735c4755d53145a54325ae82d073ghsaWEB
- www.jenkins.io/security/advisory/2022-01-12/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2022-01-12Jenkins Security Advisories · Jan 12, 2022