CVE-2022-36891
Description
Missing permission check in Deployer Framework Plugin lets attackers with Item/Read but no Deploy permission read deployment logs.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Missing permission check in Deployer Framework Plugin lets attackers with Item/Read but no Deploy permission read deployment logs.
Vulnerability
The Jenkins Deployer Framework Plugin versions 85.v1d1888e8c021 and earlier lack a permission check when serving deployment logs. The plugin fails to verify that a user possesses the required Deploy Now/Deploy permission, instead only checking Item/Read permission. This allows users with only read-level access to access sensitive deployment log data [1][2].
Exploitation
An attacker who has obtained Item/Read permission on a Jenkins instance — for example, through a compromised low-privilege account or misconfigured authorization — can directly request deployment logs. No additional authentication or privilege escalation is required beyond that initial access. The missing check exists in the log retrieval endpoint, which does not enforce the intended permission boundary [1][4].
Impact
Successful exploitation results in the unauthorized disclosure of deployment logs. These logs may contain sensitive information such as environment details, application secrets, credentials, or internal network paths. The confidentiality of deployment operations is compromised, potentially aiding further attacks [3].
Mitigation
The vulnerability is fixed in Deployer Framework Plugin version 86.v7b_a_4a_55b_f3ec and later [1][2]. Users should upgrade immediately or restrict Item/Read permission if upgrade is not possible. No workarounds are documented beyond applying the patch [3].
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:deployer-frameworkMaven | < 86.v7b_a_4a_55b_f3ec | 86.v7b_a_4a_55b_f3ec |
Affected products
2- Range: unspecified
Patches
17ba4a55bf3ecSECURITY-2205,2206,2207,2208,2764
11 files changed · +455 −1
.gitignore+2 −0 modified@@ -18,3 +18,5 @@ build/ # locally stored credentials test-keys.txt + +.DS_Store
pom.xml+7 −0 modified@@ -201,6 +201,13 @@ <scope>test</scope> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>4.6.1</version> + <scope>test</scope> + </dependency> + </dependencies> </project>
src/main/java/com/cloudbees/plugins/deployer/DeployNowRunAction.java+1 −0 modified@@ -359,6 +359,7 @@ public final Charset getCharset() { * Sends out the raw console output. */ public void doDeployText(StaplerRequest req, StaplerResponse rsp) throws IOException { + owner.getParent().checkPermission(DEPLOY); rsp.setContentType("text/plain;charset=UTF-8"); // Prevent jelly from flushing stream so Content-Length header can be added afterwards FlushProofOutputStream out = new FlushProofOutputStream(rsp.getCompressedOutputStream(req));
src/main/java/com/cloudbees/plugins/deployer/sources/FilePathValidator.java+32 −0 added@@ -0,0 +1,32 @@ +package com.cloudbees.plugins.deployer.sources; + +import hudson.FilePath; + +import java.io.File; +import java.io.IOException; + +public class FilePathValidator { + + private FilePathValidator() { + // to hide the implicit public constructor + } + + /** + * Checks whether a given child path is a descendant of a given parent path using {@link File#getCanonicalFile}. + * + * If the child path does not exist, this method will canonicalize path elements such as {@code /../} and + * {@code /./} before comparing it to the parent path, and it will not throw an exception. If the child path + * does exist, symlinks will be resolved before checking whether the child is a descendant of the parent path. + * @param child FilePath + * @param parent FilePath + * @return boolean value of whether child path is a descendant of parent path + * @throws IllegalStateException when child or parent FilePath represent remote file + * @throws IOException when {@link File#getCanonicalFile} throws + */ + public static boolean isDescendant(FilePath child, FilePath parent) throws IOException { + if (child.isRemote() || parent.isRemote()) { + throw new IllegalStateException("Directory path '" + parent + "' is not located on the controller"); + } + return new File(child.getRemote()).getCanonicalFile().toPath().startsWith(new File(parent.getRemote()).getCanonicalPath()); + } +}
src/main/java/com/cloudbees/plugins/deployer/sources/FixedDirectoryDeploySource.java+9 −1 modified@@ -31,6 +31,7 @@ import hudson.FilePath; import hudson.RelativePath; import hudson.model.AbstractProject; +import hudson.model.Item; import hudson.model.Job; import hudson.model.Run; import hudson.util.FormValidation; @@ -197,18 +198,25 @@ public FormValidation doCheckDirectoryPath(@QueryParameter @RelativePath("..") f @QueryParameter final String targetDescriptorId, @QueryParameter final String value) throws IOException, ServletException, InterruptedException { + Job job = findJob(); + if (job != null) { + job.checkPermission(Item.WORKSPACE); + } if (StringUtils.isEmpty(value)) { return FormValidation.warning("You really should specify a directory, otherwise '.' is assumed"); } if (Boolean.valueOf(fromWorkspace)) { - Job job = findJob(); if (job != null && job instanceof AbstractProject) { FilePath someWorkspace = ((AbstractProject) job).getSomeWorkspace(); if (someWorkspace == null) { return FormValidation.warning("The workspace is empty. Unable to validate '" + value + "'."); } FilePath dirPath = someWorkspace.child(value); + + if (!FilePathValidator.isDescendant(dirPath, someWorkspace)) { + return FormValidation.error("Directory path '" + value + "' is not contained within the workspace for " + job.getDisplayName()); + } if (dirPath.exists()) { if (dirPath.isDirectory()) { return delegatePathValidationToTarget(value, targetDescriptorId, dirPath);
src/main/java/com/cloudbees/plugins/deployer/sources/StaticSelectionDeploySource.java+14 −0 modified@@ -84,12 +84,20 @@ public String getFilePath() { public File getApplicationFile(@NonNull Run run) { if (run.getArtifactsDir().isDirectory()) { File file = new File(run.getArtifactsDir(), filePath); + try { + if (!FilePathValidator.isDescendant(new FilePath(file), new FilePath(run.getArtifactsDir()))) { + throw new IllegalArgumentException("Directory path '" + filePath + "' is not contained within the artifacts directory for " + run.getDisplayName()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } return file.exists() ? file : null; } else { return null; } } + /** * {@inheritDoc} */ @@ -178,6 +186,11 @@ public FormValidation doCheckFilePath(@QueryParameter final String value) "No artifacts were archived in the last successful run, unable to validate '" + value + "'"); } + FilePath artifactsDir = new FilePath(run.getArtifactsDir()); + FilePath childDir = new FilePath(artifactsDir, value); + if (!FilePathValidator.isDescendant(childDir, artifactsDir)) { + return FormValidation.error("Directory path '" + value + "' is not contained within the artifacts directory for " + run.getFullDisplayName()); + } if (new File(run.getArtifactsDir(), value).isFile()) { return FormValidation.ok(); } @@ -195,6 +208,7 @@ public ListBoxModel doFillFilePathItems() if (run != null) { FileSet fileSet = new FileSet(); + fileSet.setFollowSymlinks(false); fileSet.setProject(new Project()); fileSet.setDir(run.getArtifactsDir()); fileSet.setIncludes("**/*.war");
src/main/java/com/cloudbees/plugins/deployer/sources/WildcardPathDeploySource.java+7 −0 modified@@ -30,6 +30,7 @@ import hudson.FilePath; import hudson.RelativePath; import hudson.model.AbstractProject; +import hudson.model.Item; import hudson.model.Job; import hudson.model.Run; import hudson.util.FormValidation; @@ -89,6 +90,7 @@ public File getApplicationFile(@NonNull Run run) { File result = null; if (run.getArtifactsDir().isDirectory()) { FileSet fileSet = new FileSet(); + fileSet.setFollowSymlinks(false); fileSet.setProject(new Project()); fileSet.setDir(run.getArtifactsDir()); fileSet.setIncludes(getFilePattern()); @@ -200,6 +202,7 @@ public FormValidation doCheckFilePattern(@QueryParameter @RelativePath("..") fin if (Boolean.valueOf(fromWorkspace)) { Job job = findJob(); if (job != null && job instanceof AbstractProject) { + job.checkPermission(Item.WORKSPACE); FilePath someWorkspace = ((AbstractProject) job).getSomeWorkspace(); if (someWorkspace == null) { return FormValidation.warning("The workspace is empty. Unable to validate '" + value + "'."); @@ -209,6 +212,9 @@ public FormValidation doCheckFilePattern(@QueryParameter @RelativePath("..") fin return FormValidation.warning("Multiple files in the workspace match '" + value + "'"); } if (filePaths.length == 1) { + if (!FilePathValidator.isDescendant(filePaths[0], someWorkspace)) { + return FormValidation.error("Directory path '" + value + "' is not contained within the workspace for " + job.getDisplayName()); + } return delegatePathValidationToTarget(value, targetDescriptorId, filePaths[0]); } } @@ -219,6 +225,7 @@ public FormValidation doCheckFilePattern(@QueryParameter @RelativePath("..") fin return FormValidation.error("There are no archived artifacts"); } FileSet fileSet = new FileSet(); + fileSet.setFollowSymlinks(false); fileSet.setProject(new Project()); fileSet.setDir(run.getArtifactsDir()); fileSet.setIncludes(value);
src/test/java/com/cloudbees/plugins/deployer/DeployNowRunActionTest.java+60 −0 added@@ -0,0 +1,60 @@ +package com.cloudbees.plugins.deployer; + +import com.gargoylesoftware.htmlunit.Page; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.Result; +import jenkins.model.Jenkins; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import java.io.File; +import java.net.HttpURLConnection; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class DeployNowRunActionTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Issue("SECURITY-2205") + @Test + public void doDeployTextWhenUserWithoutPermissionThenShouldReturnStatusForbidden() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test"); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + + webClient.login("user"); + Page page = webClient.goTo("job/" + project.getName() + "/" + project.getLastSuccessfulBuild().getNumber() + "/deploy-now/deployText"); + assertThat(page.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_FORBIDDEN)); + } + + @Issue("SECURITY-2205") + @Test + public void doDeployTextWhenUserWithDeployPermissionThenShouldReturnOk() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test1"); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + File logFile = new File(project.getLastSuccessfulBuild().getRootDir() + "/cloudbees-deploy-now.log"); + logFile.createNewFile(); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() + "/" + project.getLastSuccessfulBuild().getNumber() + "/deploy-now/deployText", "text/plain"); + assertThat(page.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_OK)); + } + + @Before + public void setUpAuthorization() { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER, DeployNowRunAction.DEPLOY).everywhere().to("admin") + .grant(Jenkins.READ, Item.READ).everywhere().to("user")); + } +}
src/test/java/com/cloudbees/plugins/deployer/sources/FixedDirectoryDeploySourceTest.java+93 −0 added@@ -0,0 +1,93 @@ +package com.cloudbees.plugins.deployer.sources; + +import com.gargoylesoftware.htmlunit.Page; +import hudson.Util; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.Result; +import hudson.model.TaskListener; +import jenkins.model.Jenkins; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import java.io.File; +import java.net.HttpURLConnection; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +public class FixedDirectoryDeploySourceTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + private FreeStyleProject project; + + @Issue("SECURITY-2206") + @Test + public void doCheckDirectoryPathWhenUserWithoutPermissionThenStatusForbidden() throws Exception { + project = r.createFreeStyleProject(); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("user"); + Page page = webClient.goTo("job/" + project.getName() +"/descriptorByName/com.cloudbees.plugins.deployer.sources.FixedDirectoryDeploySource/checkDirectoryPath?fromWorkspace=true&value=value"); + + assertThat(page.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_FORBIDDEN)); + } + + @Issue("SECURITY-2206") + @Test + public void doCheckDirectoryPathWhenPathTraversalThenReturnError() throws Exception { + project = r.createFreeStyleProject(); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() +"/descriptorByName/com.cloudbees.plugins.deployer.sources.FixedDirectoryDeploySource/checkDirectoryPath?fromWorkspace=true&value=../../secret"); + + assertThat(page.getWebResponse().getContentAsString(), containsString("Directory path '../../secret' is not contained within the workspace for")); + } + + @Issue("SECURITY-2206") + @Test + public void doCheckDirectoryPathWhenValueIsSymlinkThenReturnError() throws Exception { + project = r.createFreeStyleProject(); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + Util.createSymlink(new File(project.getSomeWorkspace().getRemote()), r.jenkins.getRootDir().getAbsolutePath(), "temp_link", TaskListener.NULL); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() +"/descriptorByName/com.cloudbees.plugins.deployer.sources.FixedDirectoryDeploySource/checkDirectoryPath?fromWorkspace=true&value=temp_link"); + + assertThat(page.getWebResponse().getContentAsString(), containsString("Directory path 'temp_link' is not contained within the workspace for")); + } + + @Issue("SECURITY-2206") + @Test + public void doCheckDirectoryPathWhenParamsValidThenReturnOk() throws Exception { + project = r.createFreeStyleProject(); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + project.getSomeWorkspace().child("test").mkdirs(); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() +"/descriptorByName/com.cloudbees.plugins.deployer.sources.FixedDirectoryDeploySource/checkDirectoryPath?fromWorkspace=true&value=test"); + + assertThat(page.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_OK)); + assertThat(page.getWebResponse().getContentAsString(), is("<div/>")); + } + + @Before + public void setUpAuthorization() { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER).everywhere().to("admin") + .grant(Jenkins.READ, Item.READ).everywhere().to("user")); + } +}
src/test/java/com/cloudbees/plugins/deployer/sources/StaticSelectionDeploySourceTest.java+149 −0 added@@ -0,0 +1,149 @@ +package com.cloudbees.plugins.deployer.sources; + +import hudson.model.Run; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.nio.file.Files; + +import com.gargoylesoftware.htmlunit.Page; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.Result; +import jenkins.model.Jenkins; +import org.junit.Before; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class StaticSelectionDeploySourceTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + @Rule + public TemporaryFolder folder= new TemporaryFolder(); + + @Issue("SECURITY-2208") + @Test + public void doCheckFilePathWhenPathTraversalThenShouldReturnError() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test1"); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + + new File(project.getLastSuccessfulBuild().getRootDir() + "/archive").mkdir(); + File file = new File(project.getLastSuccessfulBuild().getRootDir() + "/archive/test.war"); + file.createNewFile(); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() + "/descriptorByName/com.cloudbees.plugins.deployer.sources.StaticSelectionDeploySource/checkFilePath?value=../log"); + + assertThat(page.getWebResponse().getContentAsString(), containsString("Directory path '../log' is not contained within the artifacts directory for")); + } + + @Issue("SECURITY-2208") + @Test + public void doCheckFilePathWhenParamIsNotPathTraversalThenShouldOk() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test1"); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + new File(project.getLastSuccessfulBuild().getRootDir() + "/archive").mkdir(); + File file = new File(project.getLastSuccessfulBuild().getRootDir() + "/archive/test.war"); + file.createNewFile(); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() + "/descriptorByName/com.cloudbees.plugins.deployer.sources.StaticSelectionDeploySource/checkFilePath?value=test.war"); + + assertThat(page.getWebResponse().getContentAsString(), is("<div/>")); + } + + @Before + public void setUpAuthorization() { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER).everywhere().to("admin") + .grant(Jenkins.READ, Item.READ).everywhere().to("user")); + } + + @Issue("SECURITY-2764") + @Test + public void fileInDirectory() throws Exception { + Run run = mock(Run.class); + File tmp = folder.newFolder(); + Files.createFile(new File(tmp,"foo.txt").toPath()); + when(run.getArtifactsDir()).thenReturn(tmp); + StaticSelectionDeploySource ssds = new StaticSelectionDeploySource("foo.txt"); + File appFile = ssds.getApplicationFile(run); + Assert.assertTrue(Files.isRegularFile(appFile.toPath())); + } + + @Issue("SECURITY-2764") + @Test + public void fileInSubDirectory() throws Exception { + Run run = mock(Run.class); + File tmp = folder.newFolder(); + File subTmp = new File(tmp, "subdir"); + Files.createDirectories(subTmp.toPath()); + Files.createFile(new File(subTmp,"foo.txt").toPath()); + when(run.getArtifactsDir()).thenReturn(tmp); + StaticSelectionDeploySource ssds = new StaticSelectionDeploySource("subdir/foo.txt"); + // the file definitely exists but it's not part of the correct directory + File appFile = ssds.getApplicationFile(run); + Assert.assertTrue(Files.isRegularFile(appFile.toPath())); + } + + + @Issue("SECURITY-2764") + @Test(expected = IllegalArgumentException.class) + public void fileNotInDirectory() throws Exception { + Run run = mock(Run.class); + File tmp = folder.newFolder(); + File subTmp = new File(tmp, "subdir"); + Files.createDirectories(subTmp.toPath()); + Files.createFile(new File(tmp,"foo.txt").toPath()); + when(run.getArtifactsDir()).thenReturn(subTmp); + StaticSelectionDeploySource ssds = new StaticSelectionDeploySource("../foo.txt"); + // the file definitely exists but it's not part of the correct directory + ssds.getApplicationFile(run); + } + + @Issue("SECURITY-2764") + @Test + public void fileNotExistingInSubdirectory() throws Exception { + Run run = mock(Run.class); + File tmp = folder.newFolder(); + when(run.getArtifactsDir()).thenReturn(tmp); + StaticSelectionDeploySource ssds = new StaticSelectionDeploySource("/etc/passwd"); + Assert.assertNull(ssds.getApplicationFile(run)); + } + + @Issue("SECURITY-2764") + @Test(expected = IllegalArgumentException.class) + public void fileInParentDirectory() throws Exception { + Run run = mock(Run.class); + when(run.getArtifactsDir()).thenReturn(new File("target")); + StaticSelectionDeploySource ssds = new StaticSelectionDeploySource("../pom.xml"); + ssds.getApplicationFile(run); + } + + @Issue("SECURITY-2764") + @Test(expected = IllegalArgumentException.class) + public void traversalPath() throws Exception { + Run run = mock(Run.class); + when(run.getArtifactsDir()).thenReturn(new File("target")); + StaticSelectionDeploySource ssds = new StaticSelectionDeploySource("../../../../../etc/passwd"); + ssds.getApplicationFile(run); + } + +}
src/test/java/com/cloudbees/plugins/deployer/sources/WildcardPathDeploySourceTest.java+81 −0 added@@ -0,0 +1,81 @@ +package com.cloudbees.plugins.deployer.sources; + +import com.gargoylesoftware.htmlunit.Page; +import hudson.Util; +import hudson.model.FreeStyleProject; +import hudson.model.Item; +import hudson.model.Result; +import hudson.model.TaskListener; +import jenkins.model.Jenkins; +import org.apache.commons.text.StringEscapeUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +import java.io.File; +import java.net.HttpURLConnection; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +public class WildcardPathDeploySourceTest { + + @Rule + public JenkinsRule r = new JenkinsRule(); + + + @Issue("SECURITY-2205") + @Test + public void doCheckFilePatternWhenUserWithoutPermissionThenStatusForbidden() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test1"); + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("user"); + Page page = webClient.goTo("job/" + project.getName() +"/descriptorByName/com.cloudbees.plugins.deployer.sources.FixedDirectoryDeploySource/checkDirectoryPath?fromWorkspace=true&value=value"); + + assertThat(page.getWebResponse().getStatusCode(), is(HttpURLConnection.HTTP_FORBIDDEN)); + } + + @Issue("SECURITY-2205") + @Test + public void doCheckFilePatternWhenValueIsSymlinkThenReturnError() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test2"); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + Util.createSymlink(new File(project.getSomeWorkspace().getRemote()), r.jenkins.getRootPath().createTempFile("prefix", "suffix").getRemote(), "master.key", TaskListener.NULL); + String value = "master.key"; + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() + "/descriptorByName/com.cloudbees.plugins.deployer.sources.WildcardPathDeploySource/checkFilePattern?fromWorkspace=true&value=" + value); + assertThat(StringEscapeUtils.unescapeHtml4(page.getWebResponse().getContentAsString()), containsString("Directory path '" + value + "' is not contained within the workspace for")); + } + + @Issue("SECURITY-2205") + @Test + public void doCheckFilePatternWhenParamsValidThenReturnOk() throws Exception { + FreeStyleProject project = r.createFreeStyleProject("test3"); + r.assertBuildStatus(Result.SUCCESS, project.scheduleBuild2(1)); + + project.getSomeWorkspace().createTempFile("prefix", "suffix"); + String value = "*suffix"; + + JenkinsRule.WebClient webClient = r.createWebClient().withThrowExceptionOnFailingStatusCode(false); + webClient.login("admin"); + Page page = webClient.goTo("job/" + project.getName() + "/descriptorByName/com.cloudbees.plugins.deployer.sources.WildcardPathDeploySource/checkFilePattern?fromWorkspace=true&value=" + value); + + assertThat(page.getWebResponse().getContentAsString(), is("<div/>")); + } + + @Before + public void setUpAuthorization() { + r.jenkins.setSecurityRealm(r.createDummySecurityRealm()); + r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy() + .grant(Jenkins.ADMINISTER).everywhere().to("admin") + .grant(Jenkins.READ, Item.READ).everywhere().to("user")); + } +}
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-rqqx-fvqx-539gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-36891ghsaADVISORY
- www.openwall.com/lists/oss-security/2022/07/27/1ghsamailing-listx_refsource_MLISTWEB
- github.com/jenkinsci/deployer-framework-plugin/commit/7ba4a55bf3ec567ee5325ea7b24b4086ac1cb3adghsaWEB
- www.jenkins.io/security/advisory/2022-07-27/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.