CVE-2023-43496
Description
Jenkins 2.423 and earlier, LTS 2.414.1 and earlier creates a temporary file in the system temporary directory with the default permissions for newly created files when installing a plugin from a URL, potentially allowing attackers with access to the system temporary directory to replace the file before it is installed in Jenkins, potentially resulting in arbitrary code execution.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins creates a temporary plugin file with insecure default permissions, enabling local attackers to replace it and achieve arbitrary code execution.
Vulnerability
Jenkins 2.423 and earlier, and LTS 2.414.1 and earlier, creates a temporary file in the system temporary directory with default permissions when installing a plugin from a URL [1][2]. This file is used to stage the plugin before installation, but because the permissions are not restricted, any user or process with access to the temporary directory can read or modify it [2].
Exploitation
An attacker with local access to the system temporary directory can replace the temporary plugin file with a malicious one before Jenkins installs it [2]. No authentication is required beyond the ability to write to the temporary directory. This is a classic time-of-check time-of-use (TOCTOU) race condition [4].
Impact
Successful exploitation allows arbitrary code execution in the context of the Jenkins process [1][2]. This could lead to full compromise of the Jenkins server and any connected systems.
Mitigation
The vulnerability is fixed in Jenkins 2.424 and LTS 2.414.2 [2]. The fix ensures the temporary file is created with restrictive permissions (owner-only read/write/execute) [4]. No workaround is available; users should upgrade immediately.
AI Insight generated on May 20, 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.main:jenkins-coreMaven | >= 2.50, < 2.414.2 | 2.414.2 |
org.jenkins-ci.main:jenkins-coreMaven | >= 2.415, < 2.424 | 2.424 |
Affected products
3- osv-coords2 versions
< 2.424.0+ 1 more
- (no CPE)range: < 2.424.0
- (no CPE)range: >= 2.50, < 2.414.2
- Jenkins Project/Jenkinsv5Range: 2.424
Patches
13 files changed · +128 −27
core/src/main/java/hudson/PluginManager.java+2 −19 modified@@ -28,8 +28,6 @@ import static hudson.init.InitMilestone.PLUGINS_LISTED; import static hudson.init.InitMilestone.PLUGINS_PREPARED; import static hudson.init.InitMilestone.PLUGINS_STARTED; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static java.util.logging.Level.FINE; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; @@ -83,18 +81,15 @@ import java.net.URLConnection; import java.net.http.HttpClient; import java.net.http.HttpRequest; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.security.CodeSource; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.EnumSet; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; @@ -1883,16 +1878,6 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl copier = new FileUploadPluginCopier(fileItem); } - if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { - Arrays.stream(Objects.requireNonNull(tmpDir.listFiles())).forEach((file -> { - try { - Files.setPosixFilePermissions(file.toPath(), EnumSet.of(OWNER_READ, OWNER_WRITE)); - } catch (IOException e) { - throw new RuntimeException(e); - } - })); - } - if ("".equals(fileName)) { return new HttpRedirect("advanced"); } @@ -1902,7 +1887,8 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl } // first copy into a temporary file name - File t = File.createTempFile("uploaded", ".jpi"); + File t = File.createTempFile("uploaded", ".jpi", tmpDir); + tmpDir.deleteOnExit(); t.deleteOnExit(); // TODO Remove this workaround after FILEUPLOAD-293 is resolved. Files.delete(Util.fileToPath(t)); @@ -1913,9 +1899,6 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl throw new ServletException(e); } copier.cleanup(); - if (!tmpDir.delete()) { - System.err.println("Failed to delete temporary directory: " + tmpDir); - } final String baseName = identifyPluginShortName(t);
test/src/test/java/hudson/PluginManagerSecurity3072Test.java+107 −0 added@@ -0,0 +1,107 @@ +package hudson; + +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import hudson.model.RootAction; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import jenkins.model.Jenkins; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +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; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +public class PluginManagerSecurity3072Test { + + @Rule + public JenkinsRule r = PluginManagerUtil.newJenkinsRule(); + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + @Issue("SECURITY-3072") + public void verifyUploadedPluginFromURLPermission() throws Exception { + assumeFalse(Functions.isWindows()); + + HtmlPage page = r.createWebClient().goTo("pluginManager/advanced"); + HtmlForm f = page.getFormByName("uploadPlugin"); + f.getInputByName("pluginUrl").setValue(Jenkins.get().getRootUrl() + "pluginManagerGetPlugin/htmlpublisher.jpi"); + r.submit(f); + + File filesRef = Files.createTempFile("tmp", ".tmp").toFile(); + File filesTmpDir = filesRef.getParentFile(); + filesRef.deleteOnExit(); + + final Set<PosixFilePermission>[] filesPermission = new Set[]{new HashSet<>()}; + await().pollInterval(250, TimeUnit.MILLISECONDS) + .atMost(10, TimeUnit.SECONDS) + .until(() -> { + Optional<File> lastUploadedPluginDir = Arrays.stream(Objects.requireNonNull( + filesTmpDir.listFiles((file, fileName) -> + fileName.startsWith("uploadDir")))). + max(Comparator.comparingLong(File::lastModified)); + if (lastUploadedPluginDir.isPresent()) { + filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPluginDir.get().toPath(), LinkOption.NOFOLLOW_LINKS); + Optional<File> pluginFile = Arrays.stream(Objects.requireNonNull( + lastUploadedPluginDir.get().listFiles((file, fileName) -> + fileName.startsWith("uploaded")))). + max(Comparator.comparingLong(File::lastModified)); + assertTrue(pluginFile.isPresent()); + return true; + } else { + return false; + } + }); + assertEquals(EnumSet.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE), filesPermission[0]); + } + + @TestExtension("verifyUploadedPluginFromURLPermission") + public static final class ReturnPluginJpiAction implements RootAction { + + @Override + public String getIconFileName() { + return "gear2.png"; + } + + @Override + public String getDisplayName() { + return "URL to retrieve a plugin jpi"; + } + + @Override + public String getUrlName() { + return "pluginManagerGetPlugin"; + } + + public void doDynamic(StaplerRequest staplerRequest, StaplerResponse staplerResponse) throws ServletException, IOException { + staplerResponse.setContentType("application/octet-stream"); + staplerResponse.setStatus(200); + staplerResponse.serveFile(staplerRequest, PluginManagerTest.class.getClassLoader().getResource("plugins/htmlpublisher.jpi")); + } + } +}
test/src/test/java/hudson/PluginManagerTest.java+19 −8 modified@@ -24,6 +24,7 @@ package hudson; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.awaitility.Awaitility.await; @@ -173,7 +174,7 @@ public String getUrlName() { } public void doDynamic(StaplerRequest staplerRequest, StaplerResponse staplerResponse) throws ServletException, IOException { - staplerResponse.setContentType("application/octet"); + staplerResponse.setContentType("application/octet-stream"); staplerResponse.setStatus(200); staplerResponse.serveFile(staplerRequest, PluginManagerTest.class.getClassLoader().getResource("plugins/htmlpublisher.jpi")); } @@ -743,24 +744,34 @@ public void verifyUploadedPluginPermission() throws Exception { File dir = tmp.newFolder(); File plugin = new File(dir, "htmlpublisher.jpi"); FileUtils.copyURLToFile(Objects.requireNonNull(getClass().getClassLoader().getResource("plugins/htmlpublisher.jpi")), plugin); - f.getInputByName("name").setValue(plugin.getAbsolutePath()); + f.getInputByName("name").setValueAttribute(plugin.getAbsolutePath()); r.submit(f); - File tmpDir = new File(File.createTempFile("tmp", ".tmp").getParent()); - tmpDir.deleteOnExit(); + File filesRef = Files.createTempFile("tmp", ".tmp").toFile(); + File filesTmpDir = filesRef.getParentFile(); + filesRef.deleteOnExit(); + final Set<PosixFilePermission>[] filesPermission = new Set[]{new HashSet<>()}; await().pollInterval(250, TimeUnit.MILLISECONDS) .atMost(10, TimeUnit.SECONDS) .until(() -> { - Optional<File> lastUploadedPlugin = Arrays.stream(Objects.requireNonNull(tmpDir.listFiles((file, fileName) -> fileName.startsWith("uploaded")))).max(Comparator.comparingLong(File::lastModified)); - if (lastUploadedPlugin.isPresent()) { - filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPlugin.get().toPath(), LinkOption.NOFOLLOW_LINKS); + Optional<File> lastUploadedPluginDir = Arrays.stream(Objects.requireNonNull( + filesTmpDir.listFiles((file, fileName) -> + fileName.startsWith("uploadDir")))). + max(Comparator.comparingLong(File::lastModified)); + if (lastUploadedPluginDir.isPresent()) { + filesPermission[0] = Files.getPosixFilePermissions(lastUploadedPluginDir.get().toPath(), LinkOption.NOFOLLOW_LINKS); + Optional<File> pluginFile = Arrays.stream(Objects.requireNonNull( + lastUploadedPluginDir.get().listFiles((file, fileName) -> + fileName.startsWith("uploaded")))). + max(Comparator.comparingLong(File::lastModified)); + assertTrue(pluginFile.isPresent()); return true; } else { return false; } }); - assertEquals(EnumSet.of(OWNER_READ, OWNER_WRITE), filesPermission[0]); + assertEquals(EnumSet.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE), filesPermission[0]); } @Test
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-55wp-3pq4-w8p9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-43496ghsaADVISORY
- www.jenkins.io/security/advisory/2023-09-20/ghsavendor-advisoryWEB
- www.openwall.com/lists/oss-security/2023/09/20/5ghsaWEB
- github.com/jenkinsci/jenkins/commit/df7c4ccda8976c06bf31b8fb9938f26fc38501caghsaWEB
News mentions
1- Jenkins Security Advisory 2023-09-20Jenkins Security Advisories · Sep 20, 2023