VYPR
Low severityNVD Advisory· Published May 17, 2022· Updated Aug 3, 2024

CVE-2022-30949

CVE-2022-30949

Description

Jenkins REPO Plugin 1.14.0 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 REPO Plugin 1.14.0 and earlier allows attackers with pipeline configuration access to check out SCM repositories using local paths, leaking other projects' SCM contents.

Vulnerability

Jenkins REPO Plugin versions 1.14.0 and earlier [1][2] allow attackers who can configure pipelines to check out SCM repositories stored on the Jenkins controller's file system by using local paths as SCM URLs [3]. The plugin does not restrict the source URL to remote repositories, enabling access to repositories located outside the intended project workspace.

Exploitation

An attacker must have the ability to configure pipelines (e.g., as a Jenkins user with Job/Configure permission) [1][3]. The attacker supplies a local file system path as the SCM URL in a pipeline step. When the pipeline executes, the REPO plugin checks out the repository at that path, copying its contents into the pipeline workspace [3].

Impact

Successful exploitation allows the attacker to obtain limited information about other projects' SCM contents stored on the Jenkins controller [1][3]. This constitutes an information disclosure vulnerability, potentially exposing source code, configuration files, or secrets from other project repositories [3]. The attacker does not need to read the controller's entire file system but can access specific local paths used by other jobs.

Mitigation

Jenkins REPO Plugin version 1.14.1, released on 2022-05-17, fixes the vulnerability by disallowing local file paths as SCM URLs [1][2]. Users should upgrade to REPO Plugin 1.14.1 or later [2]. No workaround is available for earlier versions; upgrading is the only mitigation [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.

PackageAffected versionsPatched versions
org.jenkins-ci.plugins:repoMaven
< 1.14.11.14.1
org.jenkins-ci.plugins:mercurialMaven
< 2.16.12.16.1
org.jenkins-ci.plugins:gitMaven
< 4.11.24.11.2

Affected products

4

Patches

3
8c9cbb88baff

SECURITY-2478

https://github.com/jenkinsci/repo-pluginDmitry_PlatonovMay 4, 2022via ghsa
5 files changed · +278 2
  • pom.xml+4 2 modified
    @@ -3,7 +3,7 @@
       <parent>
         <groupId>org.jenkins-ci.plugins</groupId>
         <artifactId>plugin</artifactId>
    -    <version>3.53</version>
    +    <version>4.31</version>
         <relativePath />
       </parent>
     
    @@ -16,7 +16,9 @@
       <url>https://github.com/jenkinsci/repo-plugin/blob/master/doc/README.adoc</url>
     
       <properties>
    -    <jenkins.version>2.60.3</jenkins.version>
    +    <revision>1.14.1</revision>
    +    <changelist>-SNAPSHOT</changelist>
    +    <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 {
    @@ -1082,6 +1113,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");
    @@ -1100,6 +1135,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);
    +	}
    +
     	private String getStaticManifest(final Launcher launcher,
     			final FilePath workspace, final OutputStream logger,
     			final EnvVars env)
    
  • 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”

https://github.com/jenkinsci/git-pluginDmitry_PlatonovMay 4, 2022via ghsa
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
    
55904fbb8c9d

SECURITY-2478

https://github.com/jenkinsci/mercurial-pluginDmitry_PlatonovMay 4, 2022via ghsa
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

News mentions

1