CVE-2018-1000997
Description
Jenkins 2.145 and earlier and LTS 2.138.1 and earlier are vulnerable to a Stapler path traversal allowing view access to internal object data.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Jenkins 2.145 and earlier and LTS 2.138.1 and earlier are vulnerable to a Stapler path traversal allowing view access to internal object data.
Vulnerability
CVE-2018-1000997 is a path traversal vulnerability in the Stapler web framework used by Jenkins core. It resides in multiple Stapler view handling components: core/src/main/java/org/kohsuke/stapler/Facet.java, groovy/src/main/java/org/kohsuke/stapler/jelly/groovy/GroovyFacet.java, jelly/src/main/java/org/kohsuke/stapler/jelly/JellyFacet.java, jruby/src/main/java/org/kohsuke/stapler/jelly/jruby/JRubyFacet.java, and jsp/src/main/java/org/kohsuke/stapler/jsp/JSPFacet.java. The flaw affects Jenkins 2.145 and earlier and Jenkins LTS 2.138.1 and earlier [1][4].
Exploitation
An attacker needs network access to a Jenkins instance. No authentication is required. The attacker can craft a URL path that traverses to view definitions on any type of routable object, bypassing intended view restrictions. The commit introducing the fix shows that the traversal is prevented by checking view name characters [2][3]; previously, view names like ../../secret could be used to render objects' internal views.
Impact
Successful exploitation allows an attacker to render any routable object using any view defined on its type, thereby exposing internal information not intended to be viewed. This commonly includes the toString() representation of objects, which may disclose sensitive internal data. The impact is information disclosure at the confidentiality level, without granting code execution or data modification [1][4].
Mitigation
Jenkins fixed this issue in versions 2.146 and LTS 2.138.2, released on 2018-10-10 [4]. Users should upgrade to these or later versions. Administrators who cannot immediately upgrade can set the system property org.kohsuke.stapler.Facet.allowViewNamePathTraversal to false (though the fix commit defaults it to false and adds the property for backwards compatibility) [3]. No workaround is available for unpatched versions. The vulnerability is not listed on CISA's Known Exploited Vulnerabilities (KEV) catalog.
AI Insight generated on May 22, 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.138.2 | 2.138.2 |
org.jenkins-ci.main:jenkins-coreMaven | >= 2.140, < 2.146 | 2.146 |
org.kohsuke.stapler:stapler-parentMaven | < 1.250.2 | 1.250.2 |
Affected products
3- Range: 1.324-rc, 1.325-rc, 1.327-rc, …
- ghsa-coords2 versions
< 2.138.2+ 1 more
- (no CPE)range: < 2.138.2
- (no CPE)range: < 1.250.2
Patches
25 files changed · +183 −1
core/pom.xml+1 −1 modified@@ -39,7 +39,7 @@ THE SOFTWARE. <properties> <staplerFork>true</staplerFork> - <stapler.version>1.250.1</stapler.version> + <stapler.version>1.250.2</stapler.version> <spring.version>2.5.6.SEC03</spring.version> <groovy.version>2.4.11</groovy.version> <!-- TODO: Actually many issues are being filtered by src/findbugs/findbugs-excludes.xml -->
test/src/test/java/jenkins/security/stapler/Security867Test.java+167 −0 added@@ -0,0 +1,167 @@ +/* + * The MIT License + * + * Copyright (c) 2018, CloudBees, Inc. + * + * 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 jenkins.security.stapler; + +import com.gargoylesoftware.htmlunit.Page; +import hudson.model.RootAction; +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.TestExtension; + +import javax.annotation.CheckForNull; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; + +public class Security867Test { + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + @Issue("SECURITY-867") + public void folderTraversalPrevented_avoidStealingSecretInView() throws Exception { + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + + String publicContent = "Test OK"; + String secretContent = "s3cr3t"; + + // to validate the attack reproduction you can disable the protection + // Facet.ALLOW_VIEW_NAME_PATH_TRAVERSAL = true; + + // regular behavior + assertThat(getContentAndCheck200(wc, "rootAction1/public"), containsString(publicContent)); + + // malicious usage prevention + + // looking for /jenkins/security/stapler/Security867Test/NotRootAction2/secret + assertThat(getContent(wc, "rootAction1/%2fjenkins%2fsecurity%2fstapler%2fSecurity867Test%2fNotRootAction2%2fsecret"), + not(containsString(secretContent))); + + // looking for /jenkins\security\stapler\Security867Test\NotRootAction2\secret => + // absolute path with backslash (initial forward one is required for absolute) + assertThat(getContent(wc,"rootAction1/%2fjenkins%5csecurity%5cstapler%5cSecurity867Test%5cNotRootAction2%5csecret"), + not(containsString(secretContent))); + + // looking for ../NotRootAction2/secret => relative path + assertThat(getContent(wc,"rootAction1/%2e%2e%2fNotRootAction2%2fsecret"), + not(containsString(secretContent))); + + // looking for ..\NotRootAction2\secret => relative path without forward slash + assertThat(getContent(wc, "rootAction1/%2e%2e%5cNotRootAction2%5csecret"), + not(containsString(secretContent))); + } + + private String getContent(JenkinsRule.WebClient wc, String url) throws Exception { + Page page = wc.goTo(url, null); + return page.getWebResponse().getContentAsString(); + } + + private String getContentAndCheck200(JenkinsRule.WebClient wc, String url) throws Exception { + Page page = wc.goTo(url, null); + assertThat(page.getWebResponse().getStatusCode(), equalTo(200)); + return page.getWebResponse().getContentAsString(); + } + + @Test + @Issue("SECURITY-867") + public void folderTraversalPrevented_avoidStealingSecretFromDifferentObject() throws Exception { + JenkinsRule.WebClient wc = j.createWebClient(); + wc.getOptions().setThrowExceptionOnFailingStatusCode(false); + + String action1Config = j.jenkins.getExtensionList(RootAction.class).get(RootAction1.class).getMyConfig(); + String action3Config = j.jenkins.getExtensionList(RootAction.class).get(RootAction3.class).getMyConfig(); + + // to validate the attack reproduction you can disable the protection + // Facet.ALLOW_VIEW_NAME_PATH_TRAVERSAL = true; + + // regular behavior, the config is only displayed in ActionRoot3 + assertThat(getContentAndCheck200(wc, "rootAction1/public"), not(containsString(action1Config))); + assertThat(getContentAndCheck200(wc, "rootAction3/showConfig"), allOf( + containsString(action3Config), + not(containsString(action1Config)) + )); + + // the main point here is the last node visited will be "it" for the view scope + // if we navigate by RootAction1, we pass it to the RootAction3's view + + // malicious usage prevention, looking for ../RootAction3/showConfig => relative path + // without the prevention, the config value of RootAction1 will be used here + assertThat(getContent(wc, "rootAction1/%2e%2e%2fRootAction3%2fshowConfig"), allOf( + not(containsString(action1Config)), + not(containsString(action3Config)) + )); + } + + @TestExtension + public static class RootAction1 implements RootAction { + // not displayed in its own public.jelly + public String getMyConfig() { + return "config-1"; + } + + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public @CheckForNull String getUrlName() { + return "rootAction1"; + } + } + + @TestExtension + public static class RootAction3 implements RootAction { + // displayed in its showConfig.jelly + public String getMyConfig() { + return "config-3"; + } + + @Override + public @CheckForNull String getIconFileName() { + return null; + } + + @Override + public @CheckForNull String getDisplayName() { + return null; + } + + @Override + public @CheckForNull String getUrlName() { + return "rootAction3"; + } + } +}
test/src/test/resources/jenkins/security/stapler/Security867Test/NotRootAction2/secret.jelly+5 −0 added@@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core"> + <p>s3cr3t</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/Security867Test/RootAction1/public.jelly+5 −0 added@@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core"> + <p>Test OK</p> +</j:jelly>
test/src/test/resources/jenkins/security/stapler/Security867Test/RootAction3/showConfig.jelly+5 −0 added@@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?jelly escape-by-default='true'?> +<j:jelly xmlns:j="jelly:core"> + <p>Config: ${it.myConfig}</p> +</j:jelly>
5 files changed · +48 −2
core/src/main/java/org/kohsuke/stapler/Facet.java+18 −0 modified@@ -110,6 +110,8 @@ public static <T> List<T> discoverExtensions(Class<T> type, ClassLoader... cls) public static final Logger LOGGER = Logger.getLogger(Facet.class.getName()); + public static boolean ALLOW_VIEW_NAME_PATH_TRAVERSAL = Boolean.getBoolean(Facet.class.getName() + ".allowViewNamePathTraversal"); + /** * Creates a {@link RequestDispatcher} that handles the given view, or * return null if no such view was found. @@ -144,4 +146,20 @@ public RequestDispatcher createRequestDispatcher(RequestImpl request, Class type public Klass<?> getKlass(Object o) { return null; } + + /** + * Ensure the path that is passed is only the name of the file and not a path + */ + protected boolean isBasename(String potentialPath){ + if (ALLOW_VIEW_NAME_PATH_TRAVERSAL) { + return true; + } else { + if (potentialPath.contains("\\") || potentialPath.contains("/")) { + // prevent absolute path and folder traversal to find scripts + return false; + } + + return true; + } + } }
groovy/src/main/java/org/kohsuke/stapler/jelly/groovy/GroovyFacet.java+6 −0 modified@@ -68,6 +68,11 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different if (req.getRequestURI().endsWith("/")) return false; + if (!isBasename(next)) { + // potentially an attempt to make a folder traversal + return false; + } + try { Script script = tearOff.findScript(next); if(script==null) @@ -92,6 +97,7 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I } } public String toString() { + // or TOKEN.gsp return "TOKEN.groovy for url=/TOKEN"; } });
jelly/src/main/java/org/kohsuke/stapler/jelly/JellyFacet.java+5 −0 modified@@ -77,6 +77,11 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different if (req.tokens.endsWithSlash) return false; + if (!isBasename(next)) { + // potentially an attempt to make a folder traversal + return false; + } + try { Script script = tearOff.findScript(next+".jelly");
jruby/src/main/java/org/kohsuke/stapler/jelly/jruby/JRubyFacet.java+14 −2 modified@@ -120,8 +120,20 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I if (req.tokens.countRemainingTokens()>1) return false; // and avoid serving both "foo" and "foo/" as relative URL semantics are drastically different if (req.getRequestURI().endsWith("/")) return false; - - return invokeScript(req, rsp, node, next, tearOff.findScript(next)); + + if (!isBasename(next)) { + // potentially an attempt to make a folder traversal + return false; + } + + Script script = tearOff.findScript(next); + + if (script == null) { + // no script found + return false; + } + + return invokeScript(req, rsp, node, next, script); } }); }
jsp/src/main/java/org/kohsuke/stapler/jsp/JSPFacet.java+5 −0 modified@@ -59,6 +59,11 @@ public boolean dispatch(RequestImpl req, ResponseImpl rsp, Object node) throws I Stapler stapler = req.getStapler(); + if (!isBasename(next)) { + // potentially an attempt to make a folder traversal + return false; + } + // check static resources RequestDispatcher disp = createRequestDispatcher(req,node.getClass(),node,next); if(disp==null) {
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-5hfp-964w-5vgmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-1000997ghsaADVISORY
- github.com/jenkinsci/jenkins/commit/fd5f5be0304c6bf1918892b81e2efb6b6d09c521ghsaWEB
- github.com/jenkinsci/stapler/commit/0dfc28aa2102a59638484fc11c4c53b1dbb2baf0ghsaWEB
- jenkins.io/security/advisory/2018-10-10/ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.