CVE-2022-30947
Description
Jenkins Git Plugin 4.11.1 and earlier allows attackers able to configure pipelines to check out some SCM repositories stored on the Jenkins controller's file system using local paths as SCM URLs, obtaining limited information about other projects' SCM contents.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins Git Plugin 4.11.1 and earlier allows attackers to configure pipelines to check out SCM repositories using local file paths, exposing limited information about other projects' SCM contents.
Vulnerability
Jenkins Git Plugin 4.11.1 and earlier [2] allows users with permission to configure pipelines to specify local file system paths as SCM URLs in the checkout step. When a pipeline is configured to checkout a repository using a local path (e.g., /var/jenkins_home/jobs/SomeJob/workspace), the plugin will perform git operations on that directory, potentially accessing SCM data from other projects stored on the Jenkins controller's file system [1][4]. The vulnerability is present in the plugin's handling of SCM URLs that reference local paths.
Exploitation
An attacker must have the ability to configure a Jenkins pipeline (e.g., through job configuration or Pipeline script) and specify a local file system path as the SCM URL in the checkout step. The attacker can then trigger the pipeline job, which causes the Git Plugin to read SCM metadata (such as .git directories) from the specified local path, potentially enumerating branch names, commit hashes, and other information from other projects' repositories stored on the controller [2][3]. No network-level access beyond normal Jenkins interaction is required; the attacker only needs to be able to create or modify pipeline jobs.
Impact
Successful exploitation allows the attacker to obtain limited information about SCM contents of other projects on the Jenkins controller, such as repository URLs, branch names, commit IDs, and possibly file listings from .git directories. This disclosure could aid in further attacks by revealing project structure or sensitive metadata, but does not directly lead to code execution or full data access. The information leaked is limited to what is stored in SCM metadata that is accessible via local file system paths [2][4].
Mitigation
Jenkins Git Plugin version 4.11.2, released on 2022-05-17, fixes this vulnerability by restricting the ability to use local file system paths as SCM URLs [2][3]. Users should upgrade to Git Plugin 4.11.2 or later. No known workaround is documented, and this CVE is not listed in the CISA Known Exploited Vulnerabilities catalog. Version 4.11.1 and earlier are considered end-of-life regarding this vulnerability [1].
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:gitMaven | < 4.11.2 | 4.11.2 |
org.jenkins-ci.plugins:mercurialMaven | < 2.16.1 | 2.16.1 |
org.jenkins-ci.plugins:repoMaven | < 1.15.0 | 1.15.0 |
Affected products
4- ghsa-coords3 versionspkg:maven/org.jenkins-ci.plugins/gitpkg:maven/org.jenkins-ci.plugins/mercurialpkg:maven/org.jenkins-ci.plugins/repo
< 4.11.2+ 2 more
- (no CPE)range: < 4.11.2
- (no CPE)range: < 2.16.1
- (no CPE)range: < 1.15.0
- Jenkins project/Jenkins Git Pluginv5Range: unspecified
Patches
33c8e6236b108“SECURITY-2478”
5 files changed · +276 −2
pom.xml+2 −2 modified@@ -3,7 +3,7 @@ <parent> <groupId>org.jenkins-ci.plugins</groupId> <artifactId>plugin</artifactId> - <version>3.56</version> + <version>4.31</version> <relativePath /> </parent> @@ -18,7 +18,7 @@ <properties> <revision>1.14.1</revision> <changelist>-SNAPSHOT</changelist> - <jenkins.version>2.60.3</jenkins.version> + <jenkins.version>2.277.1</jenkins.version> <java.level>8</java.level> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
src/main/java/hudson/plugins/repo/ManifestValidator.java+83 −0 added@@ -0,0 +1,83 @@ +/* + * The MIT License + * + * Copyright (c) 2010, Brad Larson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.plugins.repo; + +import hudson.AbortException; +import jenkins.util.xml.XMLUtils; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; + +import org.xml.sax.SAXException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Locale; + +/** + * to validate manifest xml file and abort when remote references a local path. + */ +public final class ManifestValidator { + + private ManifestValidator() { + // to hide the implicit public constructor + } + + /** + * to validate manifest xml file and abort when remote references a local path. + * @param manifestText byte representation of manifest file + * @param manifestRepositoryUrl url + * @throws IOException when remote references a local path. + */ + public static void validate(final byte[] manifestText, final String manifestRepositoryUrl) + throws IOException { + if (manifestText.length > 0) { + try { + Document doc = XMLUtils.parse(new ByteArrayInputStream(manifestText)); + NodeList remote = doc.getElementsByTagName("remote"); + for (int i = 0; i < remote.getLength(); i++) { + NamedNodeMap attributes = remote.item(i).getAttributes(); + for (int j = 0; j < attributes.getLength(); j++) { + if ("fetch".equals(attributes.item(j).getNodeName()) + && attributes.item(j).getNodeValue() + .toLowerCase(Locale.ENGLISH).startsWith("file://")) { + // we don't need to check source using Files.exists because fetch + // attribute could resolve only local paths starting from 'file://' + throw new AbortException("Checkout of Repo url '" + + manifestRepositoryUrl + + "' aborted because manifest references a local " + + "directory, which may be insecure. You can allow " + + "local checkouts anyway" + + " by setting the system property '" + + RepoScm.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true."); + } + } + } + } catch (SAXException e) { + throw new IOException("Could not validate manifest"); + } + } + } +}
src/main/java/hudson/plugins/repo/RepoScm.java+59 −0 modified@@ -30,16 +30,20 @@ import java.io.Serializable; import java.net.URL; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import hudson.AbortException; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; @@ -84,6 +88,17 @@ public class RepoScm extends SCM implements Serializable { private static Logger debug = Logger .getLogger("hudson.plugins.repo.RepoScm"); + /** + * escape hatch name. + */ + static final String ALLOW_LOCAL_CHECKOUT_PROPERTY = + RepoScm.class.getName() + ".ALLOW_LOCAL_CHECKOUT"; + /** + * escape hatch. + */ + //CS IGNORE VisibilityModifier FOR NEXT 1 LINES. REASON: escape hatch property. + static /* not final */ boolean ALLOW_LOCAL_CHECKOUT = Boolean.parseBoolean(System.getProperty(ALLOW_LOCAL_CHECKOUT_PROPERTY, "false")); + private final String manifestRepositoryUrl; // Advanced Fields: @@ -880,6 +895,10 @@ public void checkout( @CheckForNull final File changelogFile, @CheckForNull final SCMRevisionState baseline) throws IOException, InterruptedException { + if (!ALLOW_LOCAL_CHECKOUT && !workspace.isRemote()) { + abortIfUrlLocal(); + } + Job<?, ?> job = build.getParent(); EnvVars env = build.getEnvironment(listener); env = getEnvVars(env, job); @@ -931,6 +950,18 @@ public void checkout( build.addAction(manifestAction); } + private void abortIfUrlLocal() throws AbortException { + if (StringUtils.isNotEmpty(manifestRepositoryUrl) + && (manifestRepositoryUrl.toLowerCase(Locale.ENGLISH).startsWith("file://") + || Files.exists(Paths.get(manifestRepositoryUrl)))) { + throw new AbortException("Checkout of Repo url '" + manifestRepositoryUrl + + "' aborted because it references a local directory, " + + "which may be insecure. " + + "You can allow local checkouts anyway by setting the system property '" + + ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true."); + } + } + private int doSync(final Launcher launcher, @Nonnull final FilePath workspace, final OutputStream logger, final EnvVars env) throws IOException, InterruptedException { @@ -1093,6 +1124,10 @@ private boolean checkoutCode(final Launcher launcher, } } + if (!ALLOW_LOCAL_CHECKOUT && !workspace.isRemote()) { + abortIfManifestReferencesLocalUrl(launcher, workspace, logger, env); + } + returnCode = doSync(launcher, workspace, logger, env); if (returnCode != 0) { debug.log(Level.WARNING, "Sync failed. Resetting repository"); @@ -1111,6 +1146,30 @@ private boolean checkoutCode(final Launcher launcher, return true; } + private byte[] getManifestAsBytes(final Launcher launcher, + final FilePath workspace, + final OutputStream logger, + final EnvVars env) + throws IOException, InterruptedException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + final List<String> commands = new ArrayList<>(); + commands.add(getDescriptor().getExecutable()); + commands.add("manifest"); + launcher.launch().stderr(logger).stdout(byteArrayOutputStream).pwd(workspace) + .cmds(commands).envs(env).join(); + return byteArrayOutputStream.toByteArray(); + } + + private void abortIfManifestReferencesLocalUrl(final Launcher launcher, + final FilePath workspace, + final OutputStream logger, + final EnvVars env) + throws IOException, InterruptedException { + byte[] manifestText = getManifestAsBytes(launcher, workspace, logger, env); + + ManifestValidator.validate(manifestText, manifestRepositoryUrl); + } + /** * Adds environmental variables for the builds to the given map. */
src/test/java/hudson/plugins/repo/ManifestValidatorTest.java+56 −0 added@@ -0,0 +1,56 @@ +package hudson.plugins.repo; + +import org.junit.Test; +import org.jvnet.hudson.test.Issue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.fail; + +public class ManifestValidatorTest { + + @Issue("SECURITY-2478") + @Test + public void validateWhenFetchAttributeReferencesLocalPathThenAbort() { + String manifest = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<manifest>\n" + + " <remote name=\"local\"\n" + + " fetch=\"file:///Users/d.platonov/workdir/\"\n" + + " revision=\"master\"\n" + + " review=\"\" />\n" + + "\n" + + " <project name=\"localProject\" path=\"localProject\" groups=\"lib\" remote=\"local\" />\n" + + "</manifest>"; + try { + ManifestValidator.validate(manifest.getBytes(StandardCharsets.UTF_8), "repoUrl"); + fail("should fail because fetch attribute in remote tag references a local path"); + } catch (IOException e) { + assertThat(e.getMessage(), is("Checkout of Repo url 'repoUrl' aborted because manifest references a local directory, " + + "which may be insecure. You can allow local checkouts anyway by setting the system property '" + + RepoScm.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true.")); + } + } + + @Issue("SECURITY-2478") + @Test + public void validateWhenValidManifestThenDoNotAbort() { + String manifest = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<manifest>\n" + + " <remote name=\"origin\"\n" + + " fetch=\"..\"\n" + // https://stackoverflow.com/questions/18251358/repo-manifest-xml-what-does-the-fetch-mean + " revision=\"master\"\n" + + " review=\"https://github.com\" />\n" + + "\n" + + " <project name=\"any\" path=\"any\" groups=\"gr\" remote=\"origin\" />\n" + + " </manifest>"; + + try { + ManifestValidator.validate(manifest.getBytes(StandardCharsets.UTF_8), "repoUrl"); + } catch (Exception e) { + fail("fail because input is valid and no exception expected"); + } + } +}
src/test/java/hudson/plugins/repo/Security2478Test.java+76 −0 added@@ -0,0 +1,76 @@ +package hudson.plugins.repo; + +import hudson.model.FreeStyleBuild; +import hudson.model.FreeStyleProject; +import hudson.model.Result; +import hudson.slaves.DumbSlave; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +public class Security2478Test { + + @Rule + public JenkinsRule rule = new JenkinsRule(); + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldAbortWhenUrlIsNonRemoteAndBuildOnController() throws Exception { + FreeStyleProject freeStyleProject = rule.createFreeStyleProject(); + String manifestRepositoryUrl = testFolder.newFolder().toString(); + RepoScm scm = new RepoScm(manifestRepositoryUrl); + freeStyleProject.setScm(scm); + FreeStyleBuild freeStyleBuild = rule.assertBuildStatus(Result.FAILURE, freeStyleProject.scheduleBuild2(0)); + rule.assertLogContains("Checkout of Repo url '" + manifestRepositoryUrl + + "' aborted because it references a local directory, " + + "which may be insecure. You can allow local checkouts anyway by setting the system property '" + + RepoScm.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true.", freeStyleBuild); + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldNotAbortWhenUrlIsNonRemoteAndEscapeHatchTrue() throws Exception { + try { + RepoScm.ALLOW_LOCAL_CHECKOUT = true; + FreeStyleProject freeStyleProject = rule.createFreeStyleProject(); + String manifestRepositoryUrl = testFolder.newFolder().toString(); + RepoScm scm = new RepoScm(manifestRepositoryUrl); + freeStyleProject.setScm(scm); + FreeStyleBuild freeStyleBuild = rule.assertBuildStatus(Result.FAILURE, freeStyleProject.scheduleBuild2(0)); + + // build fails because of manifestRepositoryUrl is not a repo(git) repository, but we don't care, + // we verify that build was not aborted because of RepoScm uses local path. + rule.assertLogNotContains("Checkout of Repo url '" + manifestRepositoryUrl + + "' aborted because it references a local directory, " + + "which may be insecure. You can allow local checkouts anyway by setting the system property '" + + RepoScm.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true.", freeStyleBuild); + } finally { + RepoScm.ALLOW_LOCAL_CHECKOUT = false; + } + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldNotAbortWhenUrlIsNonRemoteAndBuildOnAgent() throws Exception { + DumbSlave agent = rule.createOnlineSlave(); + FreeStyleProject freeStyleProject = rule.createFreeStyleProject(); + + String manifestRepositoryUrl = testFolder.newFolder().toString(); + + RepoScm scm = new RepoScm(manifestRepositoryUrl); + freeStyleProject.setScm(scm); + freeStyleProject.setAssignedLabel(agent.getSelfLabel()); + + // build fails because of manifestRepositoryUrl is not a repo(git) repository, but we don't care, + // we verify that build was not aborted because of RepoScm uses local path. + rule.assertLogNotContains("Checkout of Repo url '" + manifestRepositoryUrl + + "' aborted because it references a local directory, " + + "which may be insecure. You can allow local checkouts anyway by setting the system property '" + + RepoScm.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true.", freeStyleProject.scheduleBuild2(0).get()); + } +}
b295606e0b86“SECURITY-2478”
6 files changed · +147 −0
src/main/java/hudson/plugins/git/GitSCM.java+24 −0 modified@@ -52,6 +52,7 @@ import jenkins.model.Jenkins; import jenkins.plugins.git.GitSCMMatrixUtil; import jenkins.plugins.git.GitToolChooser; +import jenkins.util.SystemProperties; import net.sf.json.JSONObject; import org.eclipse.jgit.errors.MissingObjectException; @@ -76,6 +77,8 @@ import java.io.PrintStream; import java.io.Serializable; import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Paths; import java.text.MessageFormat; import java.util.AbstractList; import java.util.ArrayList; @@ -85,6 +88,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; @@ -119,6 +123,11 @@ */ public class GitSCM extends GitSCMBackwardCompatibility { + static final String ALLOW_LOCAL_CHECKOUT_PROPERTY = GitSCM.class.getName() + ".ALLOW_LOCAL_CHECKOUT"; + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL") + public static /* not final */ boolean ALLOW_LOCAL_CHECKOUT = + SystemProperties.getBoolean(ALLOW_LOCAL_CHECKOUT_PROPERTY); + /** * Store a config version so we're able to migrate config on various * functionality upgrades. @@ -1269,6 +1278,10 @@ private boolean determineSecondFetch(CloneOption option, @NonNull RemoteConfig r public void checkout(Run<?, ?> build, Launcher launcher, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState baseline) throws IOException, InterruptedException { + if (!ALLOW_LOCAL_CHECKOUT && !workspace.isRemote()) { + abortIfSourceIsLocal(); + } + if (VERBOSE) listener.getLogger().println("Using checkout strategy: " + getBuildChooser().getDisplayName()); @@ -1380,6 +1393,17 @@ public void checkout(Run<?, ?> build, Launcher launcher, FilePath workspace, Tas } } + private void abortIfSourceIsLocal() throws AbortException { + for (UserRemoteConfig userRemoteConfig: getUserRemoteConfigs()) { + String remoteUrl = userRemoteConfig.getUrl(); + if (remoteUrl != null && (remoteUrl.toLowerCase(Locale.ENGLISH).startsWith("file://") || Files.exists(Paths.get(remoteUrl)))) { + throw new AbortException("Checkout of Git remote '" + remoteUrl + "' aborted because it references a local directory, " + + "which may be insecure. You can allow local checkouts anyway by setting the system property '" + + ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true."); + } + } + } + private void printCommitMessageToLog(TaskListener listener, GitClient git, final Build revToBuild) throws IOException { try {
src/test/java/hudson/plugins/git/extensions/GitSCMExtensionTest.java+11 −0 modified@@ -5,6 +5,7 @@ import hudson.plugins.git.GitSCM; import hudson.plugins.git.TestGitRepo; import hudson.util.StreamTaskListener; +import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; @@ -38,6 +39,16 @@ public void setUp() throws Exception { before(); } + @Before + public void allowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = true; + } + + @After + public void disallowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = false; + } + protected abstract void before() throws Exception; /**
src/test/java/hudson/plugins/git/extensions/impl/PruneStaleTagPipelineTest.java+12 −0 modified@@ -28,12 +28,14 @@ import java.util.logging.Level; import java.util.logging.Logger; +import hudson.plugins.git.GitSCM; import org.apache.commons.io.FileUtils; import org.jenkinsci.plugins.gitclient.GitClient; import org.jenkinsci.plugins.gitclient.TestCliGitAPIImpl; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -63,6 +65,16 @@ public void setup() throws Exception { listener = new LogTaskListener(Logger.getLogger("prune tags"), Level.FINEST); } + @Before + public void allowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = true; + } + + @After + public void disallowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = false; + } + @Issue("JENKINS-61869") @Test public void verify_that_local_tag_is_pruned_when_not_exist_on_remote_using_pipeline() throws Exception {
src/test/java/hudson/plugins/git/Security2478Test.java+77 −0 added@@ -0,0 +1,77 @@ +package hudson.plugins.git; + +import hudson.model.Result; +import jenkins.plugins.git.GitSampleRepoRule; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.After; +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 java.io.File; + +import static org.junit.Assert.assertFalse; + +public class Security2478Test { + + @Rule + public JenkinsRule rule = new JenkinsRule(); + + @Rule + public GitSampleRepoRule sampleRepo = new GitSampleRepoRule(); + + + @Before + public void setUpAllowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = false; + } + + @After + public void disallowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = false; + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldNotAbortWhenLocalSourceAndRunningOnAgent() throws Exception { + assertFalse("Non Remote checkout should be disallowed", GitSCM.ALLOW_LOCAL_CHECKOUT); + rule.createOnlineSlave(); + sampleRepo.init(); + sampleRepo.write("file", "v1"); + sampleRepo.git("commit", "--all", "--message=test commit"); + WorkflowJob p = rule.jenkins.createProject(WorkflowJob.class, "pipeline"); + + String script = "node('slave0') {\n" + + " checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [], userRemoteConfigs: [[url: '" + sampleRepo.fileUrl() + "', credentialsId: '']]])\n" + + "}"; + p.setDefinition(new CpsFlowDefinition(script, true)); + WorkflowRun run = rule.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0)); + rule.assertLogNotContains("aborted because it references a local directory, which may be insecure. " + + "You can allow local checkouts anyway by setting the system property 'hudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT' to true.", run); + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldAbortWhenSourceIsNonRemoteAndRunningOnController() throws Exception { + assertFalse("Non Remote checkout should be disallowed", GitSCM.ALLOW_LOCAL_CHECKOUT); + WorkflowJob p = rule.jenkins.createProject(WorkflowJob.class, "pipeline"); + String workspaceDir = rule.jenkins.getRootDir().getAbsolutePath(); + + String path = "file://" + workspaceDir + File.separator + "jobName@script" + File.separator + "anyhmachash"; + String escapedPath = path.replace("\\", "\\\\"); // for windows + String script = "node {\n" + + " checkout([$class: 'GitSCM', branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[" + + "url: '" + escapedPath + "'," + + " credentialsId: '']]])\n" + + "}"; + p.setDefinition(new CpsFlowDefinition(script, true)); + WorkflowRun run = rule.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0)); + rule.assertLogContains("Checkout of Git remote '" + path + "' " + + "aborted because it references a local directory, which may be insecure. " + + "You can allow local checkouts anyway by setting the system property 'hudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT' to true.", run); + } +}
src/test/java/jenkins/plugins/git/GitSampleRepoRule.java+11 −0 modified@@ -28,6 +28,7 @@ import com.gargoylesoftware.htmlunit.util.NameValuePair; import hudson.Launcher; import hudson.model.TaskListener; +import hudson.plugins.git.GitSCM; import hudson.util.StreamTaskListener; import java.io.ByteArrayOutputStream; import java.io.File; @@ -48,6 +49,16 @@ public final class GitSampleRepoRule extends AbstractSampleDVCSRepoRule { private static final Logger LOGGER = Logger.getLogger(GitSampleRepoRule.class.getName()); + protected void before() throws Throwable { + super.before(); + GitSCM.ALLOW_LOCAL_CHECKOUT = true; + } + + protected void after() { + super.after(); + GitSCM.ALLOW_LOCAL_CHECKOUT = false; + } + public void git(String... cmds) throws Exception { run("git", cmds); }
src/test/java/org/jenkinsci/plugins/gittagmessage/AbstractGitTagMessageExtensionTest.java+12 −0 modified@@ -3,10 +3,12 @@ import hudson.model.Job; import hudson.model.Queue; import hudson.model.Run; +import hudson.plugins.git.GitSCM; import hudson.plugins.git.util.BuildData; import jenkins.model.ParameterizedJobMixIn; import org.jenkinsci.plugins.gitclient.Git; import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -48,6 +50,16 @@ public void setUp() throws IOException, InterruptedException { repo.init(); } + @Before + public void allowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = true; + } + + @After + public void disallowNonRemoteCheckout() { + GitSCM.ALLOW_LOCAL_CHECKOUT = false; + } + @Test public void commitWithoutTagShouldNotExportMessage() throws Exception { // Given a git repo without any tags
55904fbb8c9dSECURITY-2478
2 files changed · +141 −1
src/main/java/hudson/plugins/mercurial/MercurialSCM.java+24 −1 modified@@ -11,6 +11,7 @@ import hudson.Extension; import hudson.FilePath; import hudson.Launcher; +import hudson.Main; import hudson.Util; import hudson.matrix.MatrixRun; import hudson.model.AbstractBuild; @@ -50,8 +51,11 @@ import java.io.Serializable; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; @@ -62,6 +66,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; import org.ini4j.Ini; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; @@ -74,6 +79,11 @@ */ public class MercurialSCM extends SCM implements Serializable { + static final String ALLOW_LOCAL_CHECKOUT_PROPERTY = MercurialSCM.class.getName() + ".ALLOW_LOCAL_CHECKOUT"; + //TODO: use SystemProperties instead after jenkins version upgrade to 2.236 + static /* not final */ boolean ALLOW_LOCAL_CHECKOUT = + Boolean.parseBoolean(System.getProperty(ALLOW_LOCAL_CHECKOUT_PROPERTY, String.valueOf(Main.isUnitTest))); + // Environment vars names to be exposed private static final String ENV_MERCURIAL_REVISION = "MERCURIAL_REVISION"; private static final String ENV_MERCURIAL_REVISION_SHORT = "MERCURIAL_REVISION_SHORT"; @@ -561,6 +571,11 @@ public void checkout(Run<?, ?> build, Launcher launcher, FilePath workspace, fin throws IOException, InterruptedException { MercurialInstallation mercurialInstallation = findInstallation(installation); + + if (!ALLOW_LOCAL_CHECKOUT && !workspace.isRemote()) { + abortIfSourceLocal(); + } + final boolean jobShouldUseSharing = mercurialInstallation != null && mercurialInstallation.isUseSharing(); Node node = workspaceToNode(workspace); @@ -596,7 +611,15 @@ public void checkout(Run<?, ?> build, Launcher launcher, FilePath workspace, fin } } } - + + private void abortIfSourceLocal() throws IOException { + if (StringUtils.isNotEmpty(source) && + (source.toLowerCase(Locale.ENGLISH).startsWith("file://") || Files.exists(Paths.get(source)))) { + + throw new AbortException("Checkout of Mercurial source '" + source + "' aborted because it references a local directory, which may be insecure. You can allow local checkouts anyway by setting the system property '" + ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true."); + } + } + private boolean canReuseWorkspace(FilePath repo, Node node, boolean jobShouldUseSharing, Run<?,?> build, Launcher launcher, TaskListener listener)
src/test/java/hudson/plugins/mercurial/Security2478Test.java+117 −0 added@@ -0,0 +1,117 @@ +package hudson.plugins.mercurial; + +import hudson.FilePath; +import hudson.model.Result; +import hudson.slaves.DumbSlave; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.jvnet.hudson.test.FlagRule; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.File; +import java.util.Collections; + +import static org.junit.Assert.assertFalse; + +public class Security2478Test { + + private static final String INSTALLATION = "mercurial"; + + @Rule + public JenkinsRule rule = new JenkinsRule(); + @Rule + public MercurialRule m = new MercurialRule(rule); + + @Rule + public TestRule notAllowNonRemoteCheckout = new FlagRule<>(() -> MercurialSCM.ALLOW_LOCAL_CHECKOUT, x -> MercurialSCM.ALLOW_LOCAL_CHECKOUT = x, false); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + private File repo; + + @Before + public void setUp() throws Exception { + repo = tmp.getRoot(); + rule.jenkins + .getDescriptorByType(MercurialInstallation.DescriptorImpl.class) + .setInstallations(new MercurialInstallation(INSTALLATION, "", "hg", + false, true, new File(tmp.newFolder(),"custom-dir").getAbsolutePath(), false, "", + Collections.emptyList())); + + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldAbortWhenSourceIsNonRemoteAndBuildOnController() throws Exception { + assertFalse("Non Remote checkout should be disallowed", MercurialSCM.ALLOW_LOCAL_CHECKOUT); + WorkflowJob p = rule.jenkins.createProject(WorkflowJob.class, "pipeline"); + FilePath sourcePath = rule.jenkins.getRootPath().createTempDir("t", ""); + String script = "node {\n" + + "checkout([$class: 'MercurialSCM', credentialsId: '', installation: 'mercurial', source: '" + sourcePath + "'])\n" + + "}"; + p.setDefinition(new CpsFlowDefinition(script, true)); + WorkflowRun run = rule.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0)); + rule.assertLogContains("Checkout of Mercurial source '" + sourcePath + "' aborted because it references a local directory, which may be insecure. You can allow local checkouts anyway by setting the system property '" + MercurialSCM.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true.", run); + } + + @Issue("SECURITY-2478") + @Test + public void checkoutOnAgentShouldNotAbortWhenSourceIsNonRemoteAndBuildOnAgent() throws Exception { + assertFalse("Non Remote checkout should be disallowed", MercurialSCM.ALLOW_LOCAL_CHECKOUT); + DumbSlave agent = rule.createOnlineSlave(); + FilePath workspace = agent.getRootPath().createTempDir("t", ""); + m.hg(workspace, "init"); + m.touchAndCommit(workspace, "a"); + WorkflowJob p = rule.jenkins.createProject(WorkflowJob.class, "pipeline"); + String script = "node('slave0') {\n" + + "checkout([$class: 'MercurialSCM', credentialsId: '', installation: 'mercurial', source: '" + workspace + "'])\n" + + "}"; + p.setDefinition(new CpsFlowDefinition(script, true)); + rule.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0)); + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldNotAbortWhenSourceIsAlias() throws Exception { + assertFalse("Non Remote checkout should be disallowed", MercurialSCM.ALLOW_LOCAL_CHECKOUT); + + WorkflowJob p = rule.jenkins.createProject(WorkflowJob.class, "pipeline"); + String aliasName = "alias1"; + // configure mercurial installation with an alias in a path + rule.jenkins.getDescriptorByType(MercurialInstallation.DescriptorImpl.class).setInstallations(new MercurialInstallation("mercurial", "", "hg", false, false,"", false, "[paths]\n" + aliasName + " = https://www.mercurial-scm.org/repo/hello", null)); + String script = "node {\n" + + "checkout([$class: 'MercurialSCM', credentialsId: '', installation: 'mercurial', source: '" + aliasName + "'])\n" + + "}"; + p.setDefinition(new CpsFlowDefinition(script, true)); + m.hg(new FilePath(repo), "init"); + m.touchAndCommit(new FilePath(repo), "a"); + WorkflowRun run = rule.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0)); + rule.assertLogNotContains("Checkout of Mercurial source '" + aliasName + "' aborted because it references a local directory, which may be insecure. You can allow local checkouts anyway by setting the system property '" + MercurialSCM.ALLOW_LOCAL_CHECKOUT_PROPERTY + "' to true.", run); + + } + + @Issue("SECURITY-2478") + @Test + public void checkoutShouldNotAbortWhenSourceIsAliasPointingToLocalPath() throws Exception { + assertFalse("Non Remote checkout should be disallowed", MercurialSCM.ALLOW_LOCAL_CHECKOUT); + + WorkflowJob p = rule.jenkins.createProject(WorkflowJob.class, "pipeline"); + String aliasName = "alias1"; + // configure mercurial installation with an alias in a path + rule.jenkins.getDescriptorByType(MercurialInstallation.DescriptorImpl.class).setInstallations(new MercurialInstallation("mercurial", "", "hg", false, false,"", false, "[paths]\n" + aliasName + " = " + repo.getPath(), null)); + String script = "node {\n" + + "checkout([$class: 'MercurialSCM', credentialsId: '', installation: 'mercurial', source: 'alias1'])\n" + + "}"; + p.setDefinition(new CpsFlowDefinition(script, true)); + m.hg(new FilePath(repo), "init"); + m.touchAndCommit(new FilePath(repo), "a"); + rule.assertBuildStatus(Result.SUCCESS, p.scheduleBuild2(0)); + } +}
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-84cm-vjwm-m979ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-30947ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/05/17/8ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/git-plugin/commit/b295606e0b865c298fde27bea14f9b7535a976e6ghsaWEB
- github.com/jenkinsci/mercurial-plugin/commit/55904fbb8c9d3e0b36fc26330374904cb68e8758ghsaWEB
- github.com/jenkinsci/repo-plugin/commit/3c8e6236b1088fc138a1a3e6af5ebbcb8b616f2fghsaWEB
- www.jenkins.io/security/advisory/2022-05-17/ghsax_refsource_CONFIRMWEB
News mentions
1- Jenkins Security Advisory 2022-05-17Jenkins Security Advisories · May 17, 2022