CVE-2022-29047
Description
Jenkins Pipeline: Shared Groovy Libraries Plugin 564.ve62a_4eb_b_e039 and earlier, except 2.21.3, allows attackers able to submit pull requests (or equivalent), but not able to commit directly to the configured SCM, to effectively change the Pipeline behavior by changing the definition of a dynamically retrieved library in their pull request, even if the Pipeline is configured to not trust them.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Pipeline: Shared Groovy Libraries Plugin allows untrusted pull request authors to change dynamically retrieved library definitions, bypassing trust configuration.
Vulnerability
Jenkins Pipeline: Shared Groovy Libraries Plugin versions 564.ve62a_4eb_b_e039 and earlier, except 2.21.3, contain a vulnerability that allows attackers who can submit pull requests (or equivalent actions) but lack direct commit access to the configured SCM to alter the definition of a dynamically retrieved library. This occurs even when the pipeline is configured to not trust such users, as the plugin does not properly validate the source of library definitions when retrieved via certain retriever implementations [1][2]. The issue is identified as SECURITY-1951 in the Jenkins security advisory [1].
Exploitation
An attacker with the ability to submit pull requests to the same SCM repository used by the Jenkins pipeline can create a fork or branch containing modified library code. When the pipeline executes and dynamically retrieves the library (e.g., using the library step without specifying a version, or with a version that resolves to the attacker's branch), the plugin may load the attacker's version instead of the intended trusted version. This can be achieved without requiring direct commit access or any special authentication beyond pull request submission [1][2][3][4]. The attacker does not need to be trusted by the Jenkins configuration; the vulnerability bypasses the trust mechanism.
Impact
Successful exploitation allows the attacker to effectively change the behavior of the pipeline by executing arbitrary Groovy code defined in the modified library. This can lead to arbitrary code execution within the Jenkins controller, potentially resulting in full compromise of the Jenkins environment, including disclosure of secrets, modification of jobs, and lateral movement to connected systems. The impact is high as the pipeline runs with the permissions of the Jenkins controller [1][2].
Mitigation
Jenkins has released Pipeline: Shared Groovy Libraries Plugin version 564.ve62a_4eb_b_e039 that includes a fix for this vulnerability, as documented in the Jenkins security advisory [1]. Users should upgrade to this version or later. If upgrading is not immediately possible, users can restrict the ability to submit pull requests to trusted individuals only, or configure the SCM to require trusted code reviews. However, the most effective mitigation is applying the update. No other workaround is provided [1][2][3][4].
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.workflow:workflow-cps-global-libMaven | < 2.21.3 | 2.21.3 |
org.jenkins-ci.plugins.workflow:workflow-cps-global-libMaven | >= 544.vff04fa68714d, < 566.vd0a | 566.vd0a |
Affected products
2- Jenkins project/Jenkins Pipeline: Shared Groovy Libraries Pluginv5Range: unspecified
Patches
2bae59b46cb52SECURITY-1951
4 files changed · +87 −4
src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStep.java+20 −0 modified@@ -37,6 +37,7 @@ import hudson.model.ItemGroup; import hudson.model.Run; import hudson.model.TaskListener; +import hudson.scm.SCM; import hudson.security.AccessControlled; import java.io.File; import java.io.IOException; @@ -60,6 +61,7 @@ import javax.annotation.Nonnull; import javax.inject.Inject; import jenkins.model.Jenkins; +import jenkins.scm.impl.SingleSCMSource; import org.codehaus.groovy.control.MultipleCompilationErrorsException; import org.codehaus.groovy.runtime.InvokerHelper; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.AbstractWhitelist; @@ -192,6 +194,18 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio } else if (version == null) { throw new AbortException("Must specify a version for library " + name); } + // When a user specifies a non-null retriever, they may be using SCMVar in its configuration, + // so we need to run MultibranchScmRevisionVerifier to prevent unsafe behavior. + // SCMVar would typically be used with SCMRetriever, but it is also possible to use it with SCMSourceRetriever and SingleSCMSource. + // There may be false-positive rejections if a Multibranch Pipeline for the repo of a Pipeline library + // uses the library step with a non-null retriever to check out a static version of the library. + // Fixing this would require us being able to detect usage of SCMVar precisely, which is not currently possible. + else if (retriever instanceof SCMRetriever) { + verifyRevision(((SCMRetriever) retriever).getScm(), name); + } else if (retriever instanceof SCMSourceRetriever && ((SCMSourceRetriever) retriever).getScm() instanceof SingleSCMSource) { + verifyRevision(((SingleSCMSource) ((SCMSourceRetriever) retriever).getScm()).getScm(), name); + } + LibraryRecord record = new LibraryRecord(name, version, trusted, changelog, cachingConfiguration, source); LibrariesAction action = run.getAction(LibrariesAction.class); if (action == null) { @@ -219,6 +233,12 @@ public static class Execution extends AbstractSynchronousNonBlockingStepExecutio return new LoadedClasses(name, record.getDirectoryName(), trusted, changelog, run); } + private void verifyRevision(SCM scm, String name) throws IOException, InterruptedException { + for (LibraryStepRetrieverVerifier revisionVerifier : LibraryStepRetrieverVerifier.all()) { + revisionVerifier.verify(this.run, listener, scm, name); + } + } + } public static final class LoadedClasses extends GroovyObjectSupport implements Serializable {
src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStepRetrieverVerifier.java+3 −3 renamed@@ -11,11 +11,11 @@ import java.io.IOException; @Restricted(NoExternalUse.class) -public interface SCMSourceRetrieverVerifier extends ExtensionPoint { +public interface LibraryStepRetrieverVerifier extends ExtensionPoint { void verify(Run<?, ?> run, TaskListener listener, SCM scm, String name) throws IOException, InterruptedException; - static ExtensionList<SCMSourceRetrieverVerifier> all() { - return ExtensionList.lookup(SCMSourceRetrieverVerifier.class); + static ExtensionList<LibraryStepRetrieverVerifier> all() { + return ExtensionList.lookup(LibraryStepRetrieverVerifier.class); } }
src/main/java/org/jenkinsci/plugins/workflow/libs/MultibranchScmRevisionVerifier.java+8 −1 modified@@ -1,5 +1,6 @@ package org.jenkinsci.plugins.workflow.libs; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.AbortException; import hudson.model.Job; import hudson.model.Run; @@ -17,7 +18,10 @@ import java.io.IOException; @OptionalExtension(requirePlugins={"workflow-multibranch"}) -public class MultibranchScmRevisionVerifier implements SCMSourceRetrieverVerifier { +public class MultibranchScmRevisionVerifier implements LibraryStepRetrieverVerifier { + + @SuppressFBWarnings("MS_SHOULD_BE_FINAL") // For script console and tests. + public static boolean DISABLED = Boolean.getBoolean(MultibranchScmRevisionVerifier.class.getName() + ".DISABLED"); /** * Abort library retrieval if the specified build is from a Multibranch Pipeline configured to build the library's SCM and the revision being built is untrusted. @@ -26,6 +30,9 @@ public class MultibranchScmRevisionVerifier implements SCMSourceRetrieverVerifie */ @Override public void verify(Run<?, ?> run, TaskListener listener, SCM libraryScm, String name) throws IOException, InterruptedException { + if (DISABLED) { + return; + } // Adapted from ReadTrustedStep Job<?, ?> job = run.getParent(); BranchJobProperty property = job.getProperty(BranchJobProperty.class);
src/test/java/org/jenkinsci/plugins/workflow/libs/SCMRetrieverTest.java+56 −0 modified@@ -124,6 +124,62 @@ public class SCMRetrieverTest { r.assertLogContains("Library '" + libraryName + "' has been modified in an untrusted revision", run); } + @Test public void libraryCanBeRetrievedStaticallyEvenWhenPipelineScmUntrusted() throws Exception { + sampleRepo.init(); + sampleRepo.write("vars/greet.groovy", "def call(recipient) {echo(/hello from $recipient/)}"); + sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'master'}}"); + sampleRepo.write("Jenkinsfile", "greet(pkg.Clazz.whereAmI())"); // Library loaded implicitly. + sampleRepo.git("add", "vars", "src", "Jenkinsfile"); + sampleRepo.git("commit", "--message=init"); + + sampleRepo.git("checkout", "-b", "fork"); + sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'fork'}}"); + sampleRepo.git("commit", "--all", "--message=branching"); + + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mp"); + String libraryName = "stuff"; + LibraryConfiguration config = new LibraryConfiguration(libraryName, new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true))); + config.setDefaultVersion("master"); + config.setImplicit(true); + GlobalLibraries.get().setLibraries(Collections.singletonList(config)); + + SCMSource warySource = new WarySource(sampleRepo.toString()); + mp.getSourcesList().add(new BranchSource(warySource)); + WorkflowJob job = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "fork"); + r.waitUntilNoActivity(); + WorkflowRun run = job.getLastBuild(); + // The fork is untrusted, but that doesn't matter because we are using stuff@master, which the untrusted user can't modify. + r.assertBuildStatus(Result.SUCCESS, run); + r.assertLogContains("hello from master", run); + } + + @Issue("SECURITY-1951") + @Test public void libraryCantBeRetrievedWithoutVersionUsingScmSourceRetriever() throws Exception { + sampleRepo.init(); + sampleRepo.write("vars/greet.groovy", "def call(recipient) {echo(/hello to $recipient/)}"); + sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'master'}}"); + sampleRepo.write("Jenkinsfile", "def lib = library(identifier: 'stuff@master', retriever: modernSCM(fromScm(name: 'master', scm: scm))); greet(lib.pkg.Clazz.whereAmI())"); + sampleRepo.git("add", "vars", "src", "Jenkinsfile"); + sampleRepo.git("commit", "--message=init"); + + sampleRepo.git("checkout", "-b", "fork"); + sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'fork'}}"); + sampleRepo.git("commit", "--all", "--message=branching"); + + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mp"); + String libraryName = "stuff"; + mp.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration(libraryName, new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)))))); + + SCMSource warySource = new WarySource(sampleRepo.toString()); + mp.getSourcesList().add(new BranchSource(warySource)); + WorkflowJob job = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "fork"); + r.waitUntilNoActivity(); + WorkflowRun run = job.getLastBuild(); + + r.assertBuildStatus(Result.FAILURE, run); + r.assertLogContains("Library '" + libraryName + "' has been modified in an untrusted revision", run); + } + public static class WarySource extends GitSCMSource { public WarySource(String remote) {
97bf32458e60[SECURITY-1951]
4 files changed · +149 −15
pom.xml+15 −15 modified@@ -125,31 +125,36 @@ <artifactId>workflow-support</artifactId> </dependency> - <!-- test only plugins --> <dependency> - <groupId>org.jenkins-ci.plugins.workflow</groupId> - <artifactId>workflow-support</artifactId> - <classifier>tests</classifier> - <scope>test</scope> + <groupId>org.jenkins-ci.plugins</groupId> + <artifactId>variant</artifactId> </dependency> <dependency> <groupId>org.jenkins-ci.plugins.workflow</groupId> - <artifactId>workflow-job</artifactId> - <scope>test</scope> + <artifactId>workflow-multibranch</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>org.jenkins-ci.plugins</groupId> + <artifactId>branch-api</artifactId> + <optional>true</optional> </dependency> + + <!-- test only plugins --> <dependency> <groupId>org.jenkins-ci.plugins.workflow</groupId> - <artifactId>workflow-basic-steps</artifactId> + <artifactId>workflow-support</artifactId> + <classifier>tests</classifier> <scope>test</scope> </dependency> <dependency> <groupId>org.jenkins-ci.plugins.workflow</groupId> - <artifactId>workflow-durable-task-step</artifactId> + <artifactId>workflow-basic-steps</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.jenkins-ci.plugins.workflow</groupId> - <artifactId>workflow-multibranch</artifactId> + <artifactId>workflow-durable-task-step</artifactId> <scope>test</scope> </dependency> <dependency> @@ -204,11 +209,6 @@ <version>1.10.1</version> <scope>test</scope> </dependency> - <dependency> - <groupId>org.jenkins-ci.plugins</groupId> - <artifactId>branch-api</artifactId> - <scope>test</scope> - </dependency> </dependencies> <build> <plugins>
src/main/java/org/jenkinsci/plugins/workflow/libs/MultibranchScmRevisionVerifier.java+60 −0 added@@ -0,0 +1,60 @@ +package org.jenkinsci.plugins.workflow.libs; + +import hudson.AbortException; +import hudson.model.Job; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.scm.SCM; +import jenkins.branch.Branch; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMRevisionAction; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.SCMSourceOwner; +import org.jenkinsci.plugins.variant.OptionalExtension; +import org.jenkinsci.plugins.workflow.multibranch.BranchJobProperty; + +import java.io.IOException; + +@OptionalExtension(requirePlugins={"workflow-multibranch"}) +public class MultibranchScmRevisionVerifier implements SCMSourceRetrieverVerifier { + + /** + * Abort library retrieval if the specified build is from a Multibranch Pipeline configured to build the library's SCM and the revision being built is untrusted. + * Comparable to the defenses against untrusted users in {@code SCMBinder}, but here we care about the library rather than the Jenkinsfile. + * @throws AbortException if the specified build is from a Multibranch Pipeline configured to build the library's SCM and the revision being built is untrusted + */ + @Override + public void verify(Run<?, ?> run, TaskListener listener, SCM libraryScm, String name) throws IOException, InterruptedException { + // Adapted from ReadTrustedStep + Job<?, ?> job = run.getParent(); + BranchJobProperty property = job.getProperty(BranchJobProperty.class); + if (property == null || !(job.getParent() instanceof SCMSourceOwner)) { + // Not a multibranch project, so we do not care. + // It is possible to use legacySCM(scm) from a non-multibranch Pipeline that uses CpsScmFlowDefinition, + // but in that case we implicitly trust the changes because only a user with Item/Configure permission can select which branches to build. + return; + } + Branch pipelineBranch = property.getBranch(); + SCMSource pipelineScmSource = ((SCMSourceOwner)job.getParent()).getSCMSource(pipelineBranch.getSourceId()); + if (pipelineScmSource == null) { + throw new IllegalStateException(pipelineBranch.getSourceId() + " not found"); + } + SCMHead head = pipelineBranch.getHead(); + SCMRevision headRevision; + SCMRevisionAction action = run.getAction(SCMRevisionAction.class); + if (action != null) { + headRevision = action.getRevision(); + } else { + headRevision = pipelineScmSource.fetch(head, listener); + if (headRevision == null) { + throw new AbortException("Could not determine exact tip revision of " + pipelineBranch.getName()); + } + run.addAction(new SCMRevisionAction(pipelineScmSource, headRevision)); + } + SCMRevision trustedRevision = pipelineScmSource.getTrustedRevision(headRevision, listener); + if (!headRevision.equals(trustedRevision) && libraryScm.getKey().equals(pipelineScmSource.build(head, headRevision).getKey())) { + throw new AbortException("Library '" + name + "' has been modified in an untrusted revision"); + } + } +}
src/main/java/org/jenkinsci/plugins/workflow/libs/SCMSourceRetrieverVerifier.java+21 −0 added@@ -0,0 +1,21 @@ +package org.jenkinsci.plugins.workflow.libs; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.scm.SCM; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import java.io.IOException; + +@Restricted(NoExternalUse.class) +public interface SCMSourceRetrieverVerifier extends ExtensionPoint { + + void verify(Run<?, ?> run, TaskListener listener, SCM scm, String name) throws IOException, InterruptedException; + + static ExtensionList<SCMSourceRetrieverVerifier> all() { + return ExtensionList.lookup(SCMSourceRetrieverVerifier.class); + } +}
src/test/java/org/jenkinsci/plugins/workflow/libs/SCMRetrieverTest.java+53 −0 modified@@ -24,11 +24,18 @@ package org.jenkinsci.plugins.workflow.libs; +import java.io.IOException; import java.util.Collections; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.model.Result; +import hudson.model.TaskListener; import jenkins.branch.BranchSource; import jenkins.plugins.git.GitSCMSource; import jenkins.plugins.git.GitSampleRepoRule; import jenkins.plugins.git.traits.BranchDiscoveryTrait; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; +import jenkins.scm.api.SCMSource; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject; @@ -38,6 +45,7 @@ import org.junit.Test; import org.junit.Rule; import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.Url; @@ -89,4 +97,49 @@ public class SCMRetrieverTest { r.assertLogContains("hello to earth", r.waitForCompletion(m1)); } + @Issue("SECURITY-1951") + @Test public void untrustedUsersCanOverideLibraryWithOtherSource() throws Exception { + sampleRepo.init(); + sampleRepo.write("vars/greet.groovy", "def call(recipient) {echo(/hello to $recipient/)}"); + sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'master'}}"); + sampleRepo.write("Jenkinsfile", "def lib = library identifier: 'stuff@snapshot', retriever: legacySCM(scm); greet(lib.pkg.Clazz.whereAmI())"); + sampleRepo.git("add", "vars", "src", "Jenkinsfile"); + sampleRepo.git("commit", "--message=init"); + + sampleRepo.git("checkout", "-b", "fork"); + sampleRepo.write("src/pkg/Clazz.groovy", "package pkg; class Clazz {static String whereAmI() {'fork'}}"); + sampleRepo.git("commit", "--all", "--message=branching"); + + WorkflowMultiBranchProject mp = r.jenkins.createProject(WorkflowMultiBranchProject.class, "mp"); + String libraryName = "stuff"; + mp.getProperties().add(new FolderLibraries(Collections.singletonList(new LibraryConfiguration(libraryName, new SCMSourceRetriever(new GitSCMSource(null, sampleRepo.toString(), "", "*", "", true)))))); + + + SCMSource warySource = new WarySource(sampleRepo.toString()); + mp.getSourcesList().add(new BranchSource(warySource)); + WorkflowJob job = WorkflowMultiBranchProjectTest.scheduleAndFindBranchProject(mp, "fork"); + r.waitUntilNoActivity(); + WorkflowRun run = job.getLastBuild(); + r.assertBuildStatus(Result.FAILURE, run); + r.assertLogContains("Library '" + libraryName + "' has been modified in an untrusted revision", run); + } + + public static class WarySource extends GitSCMSource { + + public WarySource(String remote) { + super(null, remote, "", "*", "", false); + } + @Override + @NonNull + public SCMRevision getTrustedRevision(@NonNull SCMRevision revision, @NonNull TaskListener listener) throws IOException, InterruptedException { + String branch = revision.getHead().getName(); + if (branch.equals("master")) { + return revision; + } else { + listener.getLogger().println("not trusting " + branch); + return fetch(new SCMHead("master"), listener); + } + } + } + }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-hh6f-6fp5-gfpvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-29047ghsaADVISORY
- github.com/jenkinsci/workflow-cps-global-lib-plugin/commit/97bf32458e60ad252cfe5e7949bacf04459cee64ghsaWEB
- github.com/jenkinsci/workflow-cps-global-lib-plugin/commit/bae59b46cb524549d7f346ba73d3161804c97331ghsaWEB
- www.jenkins.io/security/advisory/2022-04-12/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2022-04-12Jenkins Security Advisories · Apr 12, 2022