CVE-2022-20620
Description
Missing permission checks in Jenkins SSH Agent Plugin 1.23 and earlier allows attackers with Overall/Read access to enumerate credentials IDs of credentials stored in Jenkins.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins SSH Agent Plugin 1.23 and earlier lacks permission checks, allowing attackers with Overall/Read access to enumerate credential IDs.
Vulnerability
Jenkins SSH Agent Plugin versions 1.23 and earlier contain missing permission checks in a method that populates credential items. This allows attackers with Overall/Read access to enumerate credentials IDs stored in Jenkins, leading to information disclosure. The affected code path is reachable via the doFillCredentialsItems method in the SSHAgentStep descriptor. [1][2] [3] [4]
Exploitation
An attacker needs only Overall/Read access to the Jenkins instance. By sending a crafted request to the relevant form filling method (e.g., doFillCredentialsItems), the attacker can retrieve a list of credential IDs without needing the additional permissions normally required to view credential details. No user interaction or special conditions beyond possessing the basic Overall/Read permission are required. [1][3][4]
Impact
Successful exploitation results in the disclosure of credential IDs. While the actual credential secrets (e.g., private keys, passwords) are not directly exposed, knowledge of credential IDs can aid in further attacks, such as using the IDs in subsequent operations (e.g., assigning credentials to jobs) if the attacker has additional permissions. The CIA impact is primarily confidentiality (information disclosure) of credential metadata. [1][2]
Mitigation
The Jenkins Security Advisory 2022-01-12 and the OSS-Security announcement indicate that the fix is included in SSH Agent Plugin version 1.23.2. Users should upgrade to 1.23.2 or later. No workarounds are mentioned in the available references. [1][2]
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:ssh-agentMaven | >= 1.23, < 1.23.2 | 1.23.2 |
org.jenkins-ci.plugins:ssh-agentMaven | < 1.22.1 | 1.22.1 |
Affected products
3- Range: <=1.23
- Range: unspecified
Patches
29c08b9f93cfbSECURITY-2189
3 files changed · +150 −7
src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java+8 −4 modified@@ -44,6 +44,7 @@ import hudson.model.Queue; import hudson.model.queue.Tasks; import hudson.security.ACL; +import hudson.security.AccessControlled; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.util.IOException2; @@ -63,8 +64,9 @@ import javax.annotation.Nonnull; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.QueryParameter; /** * A build wrapper that provides an SSH agent using supplied credentials @@ -496,8 +498,11 @@ public String getDisplayName() { * @return the list box model. */ @SuppressWarnings("unused") // used by stapler - public ListBoxModel doFillIdItems() { - Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + public ListBoxModel doFillIdItems(@AncestorInPath Item item) { + AccessControlled contextToCheck = item == null ? Jenkins.get() : item; + if (!contextToCheck.hasPermission(CredentialsProvider.VIEW)) { + return new StandardUsernameListBoxModel(); + } return new StandardUsernameListBoxModel() .includeMatchingAs( item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, @@ -507,7 +512,6 @@ public ListBoxModel doFillIdItems() { SSHAuthenticator.matcher() ); } - } }
src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep.java+9 −3 modified@@ -10,12 +10,15 @@ import hudson.model.Queue; import hudson.model.queue.Tasks; import hudson.security.ACL; +import hudson.security.AccessControlled; import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.QueryParameter; import java.io.Serializable; import java.util.Collections; @@ -76,8 +79,11 @@ public boolean takesImplicitBlockArgument() { * @return the list box model. */ @SuppressWarnings("unused") // used by stapler - public ListBoxModel doFillCredentialsItems() { - Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + public ListBoxModel doFillCredentialsItems(@AncestorInPath Item item) { + AccessControlled contextToCheck = item == null ? Jenkins.get() : item; + if (!contextToCheck.hasPermission(CredentialsProvider.VIEW)) { + return new StandardUsernameListBoxModel(); + } return new StandardUsernameListBoxModel() .includeMatchingAs( item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task)item) : ACL.SYSTEM,
src/test/java/com/cloudbees/jenkins/plugins/sshagent/Security2189Test.java+133 −0 added@@ -0,0 +1,133 @@ +package com.cloudbees.jenkins.plugins.sshagent; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.User; +import hudson.security.ACL; +import hudson.security.ACLContext; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +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 java.io.IOException; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test if there is any information disclosure + */ +public class Security2189Test { + + private static final String SECURE_DATA = "SecureData"; + private static final String TEST_NAME = "test"; + private static final String ADMINISTER_NAME = "administer"; + private static final String WITHOUT_ANY_PERMISSION_USER_NAME = "WithoutAnyPermissionUser"; + + @Rule + public JenkinsRule r = new JenkinsRule(); + + private WorkflowJob job; + private FreeStyleProject project; + + @Issue("SECURITY-2189") + @Test + public void doFillCredentialsItemsWhenAdminThenListPopulatedWithNameAndId() throws IOException { + setUpAuthorizationAndWorkflowJob(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(ADMINISTER_NAME))) { + SSHAgentStep.DescriptorImpl descriptor = (SSHAgentStep.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentStep.class); + ListBoxModel secureData = descriptor.doFillCredentialsItems(job); + ListBoxModel expected = new ListBoxModel(new ListBoxModel.Option(TEST_NAME, SECURE_DATA)); + + assertListBoxModel(secureData, expected); + } + } + + @Issue("SECURITY-2189") + @Test + public void doFillCredentialsItemsWhenUserWithoutAnyCredentialsThenListNotPopulated() throws Exception { + setUpAuthorizationAndWorkflowJob(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(WITHOUT_ANY_PERMISSION_USER_NAME))) { + SSHAgentStep.DescriptorImpl descriptor = (SSHAgentStep.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentStep.class); + ListBoxModel secureData = descriptor.doFillCredentialsItems(job); + + assertThat(secureData, is(empty())); + } + } + + @Issue("SECURITY-2189") + @Test + public void doFillIdItemsWhenAdminThenListPopulated() throws IOException { + setUpAuthorizationAndFreestyleProject(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(ADMINISTER_NAME))) { + SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl descriptor = (SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentBuildWrapper.CredentialHolder.class); + ListBoxModel secureData = descriptor.doFillIdItems(project); + ListBoxModel expected = new ListBoxModel(new ListBoxModel.Option(TEST_NAME, SECURE_DATA)); + + assertListBoxModel(secureData, expected); + } + } + + @Issue("SECURITY-2189") + @Test + public void doFillIdItemsWhenUserWithoutAnyPermissionThenListNotPopulated() throws Exception { + setUpAuthorizationAndWorkflowJob(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(WITHOUT_ANY_PERMISSION_USER_NAME))) { + SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl descriptor = (SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentBuildWrapper.CredentialHolder.class); + ListBoxModel secureData = descriptor.doFillIdItems(project); + + assertThat(secureData, is(empty())); + } + } + + private void setUpAuthorizationAndWorkflowJob() throws IOException { + job = r.jenkins.createProject(WorkflowJob.class, "j"); + setUpAuthorization(job); + } + + private void setUpAuthorizationAndFreestyleProject() throws IOException { + project = r.jenkins.createProject(FreeStyleProject.class, "p"); + setUpAuthorization(project); + } + + private void setUpAuthorization(Item... items) { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER).everywhere().to(ADMINISTER_NAME) + .grant().onItems(items).to(WITHOUT_ANY_PERMISSION_USER_NAME)); + } + + private static void assertListBoxModel(ListBoxModel actual, ListBoxModel expected) { + assertThat(actual, is(not(empty()))); + assertThat(actual, hasSize(expected.size())); + assertThat(actual.get(0).name, is(expected.get(0).name)); + assertThat(actual.get(0).value, is(expected.get(0).value)); + } + + private static void initCredentials(String credentialsId, String name) throws IOException { + SSHUserPrivateKey key = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, credentialsId, "cloudbees", + null, "* .*", name); + SystemCredentialsProvider.getInstance().getCredentials().add(key); + SystemCredentialsProvider.getInstance().save(); + } +}
04f526d2f73aSECURITY-2189
3 files changed · +151 −7
src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentBuildWrapper.java+8 −4 modified@@ -44,6 +44,7 @@ import hudson.model.Queue; import hudson.model.queue.Tasks; import hudson.security.ACL; +import hudson.security.AccessControlled; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.util.IOException2; @@ -63,8 +64,9 @@ import javax.annotation.Nonnull; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.QueryParameter; /** * A build wrapper that provides an SSH agent using supplied credentials @@ -496,8 +498,11 @@ public String getDisplayName() { * @return the list box model. */ @SuppressWarnings("unused") // used by stapler - public ListBoxModel doFillIdItems() { - Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + public ListBoxModel doFillIdItems(@AncestorInPath Item item) { + AccessControlled contextToCheck = item == null ? Jenkins.get() : item; + if (!contextToCheck.hasPermission(CredentialsProvider.VIEW)) { + return new StandardUsernameListBoxModel(); + } return new StandardUsernameListBoxModel() .includeMatchingAs( item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task) item) : ACL.SYSTEM, @@ -507,7 +512,6 @@ public ListBoxModel doFillIdItems() { SSHAuthenticator.matcher() ); } - } }
src/main/java/com/cloudbees/jenkins/plugins/sshagent/SSHAgentStep.java+9 −3 modified@@ -10,12 +10,15 @@ import hudson.model.Queue; import hudson.model.queue.Tasks; import hudson.security.ACL; +import hudson.security.AccessControlled; import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; import org.jenkinsci.plugins.workflow.steps.AbstractStepDescriptorImpl; import org.jenkinsci.plugins.workflow.steps.AbstractStepImpl; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; -import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.QueryParameter; import java.io.Serializable; import java.util.Collections; @@ -76,8 +79,11 @@ public boolean takesImplicitBlockArgument() { * @return the list box model. */ @SuppressWarnings("unused") // used by stapler - public ListBoxModel doFillCredentialsItems() { - Item item = Stapler.getCurrentRequest().findAncestorObject(Item.class); + public ListBoxModel doFillCredentialsItems(@AncestorInPath Item item) { + AccessControlled contextToCheck = item == null ? Jenkins.get() : item; + if (!contextToCheck.hasPermission(CredentialsProvider.VIEW)) { + return new StandardUsernameListBoxModel(); + } return new StandardUsernameListBoxModel() .includeMatchingAs( item instanceof Queue.Task ? Tasks.getAuthenticationOf((Queue.Task)item) : ACL.SYSTEM,
src/test/java/com/cloudbees/jenkins/plugins/sshagent/Security2189Test.java+134 −0 added@@ -0,0 +1,134 @@ +package com.cloudbees.jenkins.plugins.sshagent; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.SystemCredentialsProvider; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.User; +import hudson.security.ACL; +import hudson.security.ACLContext; +import hudson.util.ListBoxModel; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +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 java.io.IOException; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; + +/** + * Test if there is any information disclosure + */ +public class Security2189Test { + + private static final String SECURE_DATA = "SecureData"; + private static final String TEST_NAME = "test"; + private static final String ADMINISTER_NAME = "administer"; + private static final String WITHOUT_ANY_PERMISSION_USER_NAME = "WithoutAnyPermissionUser"; + public static final String USERNAME = "cloudbees"; + + @Rule + public JenkinsRule r = new JenkinsRule(); + + private WorkflowJob job; + private FreeStyleProject project; + + @Issue("SECURITY-2189") + @Test + public void doFillCredentialsItemsWhenAdminThenListPopulatedWithNameAndId() throws IOException { + setUpAuthorizationAndWorkflowJob(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(ADMINISTER_NAME))) { + SSHAgentStep.DescriptorImpl descriptor = (SSHAgentStep.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentStep.class); + ListBoxModel secureData = descriptor.doFillCredentialsItems(job); + ListBoxModel expected = new ListBoxModel(new ListBoxModel.Option(USERNAME + " (" + TEST_NAME + ")", SECURE_DATA)); + + assertListBoxModel(secureData, expected); + } + } + + @Issue("SECURITY-2189") + @Test + public void doFillCredentialsItemsWhenUserWithoutAnyCredentialsThenListNotPopulated() throws Exception { + setUpAuthorizationAndWorkflowJob(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(WITHOUT_ANY_PERMISSION_USER_NAME))) { + SSHAgentStep.DescriptorImpl descriptor = (SSHAgentStep.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentStep.class); + ListBoxModel secureData = descriptor.doFillCredentialsItems(job); + + assertThat(secureData, is(empty())); + } + } + + @Issue("SECURITY-2189") + @Test + public void doFillIdItemsWhenAdminThenListPopulated() throws IOException { + setUpAuthorizationAndFreestyleProject(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(ADMINISTER_NAME))) { + SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl descriptor = (SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentBuildWrapper.CredentialHolder.class); + ListBoxModel secureData = descriptor.doFillIdItems(project); + ListBoxModel expected = new ListBoxModel(new ListBoxModel.Option(USERNAME + " (" + TEST_NAME + ")", SECURE_DATA)); + + assertListBoxModel(secureData, expected); + } + } + + @Issue("SECURITY-2189") + @Test + public void doFillIdItemsWhenUserWithoutAnyPermissionThenListNotPopulated() throws Exception { + setUpAuthorizationAndWorkflowJob(); + initCredentials(SECURE_DATA, TEST_NAME); + + try(ACLContext aclContext = ACL.as(User.getOrCreateByIdOrFullName(WITHOUT_ANY_PERMISSION_USER_NAME))) { + SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl descriptor = (SSHAgentBuildWrapper.CredentialHolder.DescriptorImpl) Jenkins.get().getDescriptorOrDie(SSHAgentBuildWrapper.CredentialHolder.class); + ListBoxModel secureData = descriptor.doFillIdItems(project); + + assertThat(secureData, is(empty())); + } + } + + private void setUpAuthorizationAndWorkflowJob() throws IOException { + job = r.jenkins.createProject(WorkflowJob.class, "j"); + setUpAuthorization(job); + } + + private void setUpAuthorizationAndFreestyleProject() throws IOException { + project = r.jenkins.createProject(FreeStyleProject.class, "p"); + setUpAuthorization(project); + } + + private void setUpAuthorization(Item... items) { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER).everywhere().to(ADMINISTER_NAME) + .grant().onItems(items).to(WITHOUT_ANY_PERMISSION_USER_NAME)); + } + + private static void assertListBoxModel(ListBoxModel actual, ListBoxModel expected) { + assertThat(actual, is(not(empty()))); + assertThat(actual, hasSize(expected.size())); + assertThat(actual.get(0).name, is(expected.get(0).name)); + assertThat(actual.get(0).value, is(expected.get(0).value)); + } + + private static void initCredentials(String credentialsId, String name) throws IOException { + SSHUserPrivateKey key = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, credentialsId, USERNAME, + null, "* .*", name); + SystemCredentialsProvider.getInstance().getCredentials().add(key); + SystemCredentialsProvider.getInstance().save(); + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-9wxh-jjj5-67cvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-20620ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/01/12/6ghsamailing-listx_refsource_MLISTWEB
- github.com/CVEProject/cvelist/blob/2d78eb36f4d084db7fb35f1535d8d84fdcb7d859/2022/20xxx/CVE-2022-20620.jsonhttps://github.com/CVEProject/cvelist/blob/2d78eb36f4d084db7fb35f1535d8d84fdcb7d859/2022/20xxx/CVE-2022-20620.jsonghsaWEB
- github.com/jenkinsci/ssh-agent-plugin/commit/04f526d2f73a6fc24b59df20ba03d95265114835ghsaWEB
- github.com/jenkinsci/ssh-agent-plugin/commit/9c08b9f93cfb3ada0f0124f5bd7f0d027728a750ghsaWEB
- 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