Improper Neutralization of Input During Web Page Generation in Jenkins
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
1.626 → fixed in 1.6400 → fixed in 1.625.2
Details
Cross-site scripting (XSS) vulnerability in Jenkins before 1.640 and LTS before 1.625.2 allows remote authenticated users to inject arbitrary web script or HTML via unspecified vectors related to workspaces and archived artifacts.
The fix
[FIX SECURITY-95] Add CSP headers to files served by Jenkins
core/src/main/java/hudson/model/DirectoryBrowserSupport.java+16 −0
@@ -47,6 +47,8 @@import org.apache.commons.io.IOUtils;import org.apache.tools.zip.ZipEntry;import org.apache.tools.zip.ZipOutputStream;+import org.kohsuke.accmod.Restricted;+import org.kohsuke.accmod.restrictions.NoExternalUse;import org.kohsuke.stapler.HttpResponse;import org.kohsuke.stapler.StaplerRequest;import org.kohsuke.stapler.StaplerResponse;@@ -307,6 +309,17 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root// pseudo file name to let the Stapler set text/plainrsp.serveFile(req, in, lastModified, -1, length, "plain.txt");} else {+String csp = System.getProperty(DirectoryBrowserSupport.class.getName() + ".CSP");+if (csp == null) {+// default value unless overridden with system property+csp = DEFAULT_CSP_VALUE;+}+if (!csp.trim().equals("")) {+// allow users to prevent sending this header by setting empty system property+for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {+rsp.setHeader(header, csp);+}+}rsp.serveFile(req, in, lastModified, -1, length, baseFile.getName() );}}@@ -574,4 +587,7 @@ private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, Lisprivate static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());++@Restricted(NoExternalUse.class)+public static final String DEFAULT_CSP_VALUE = "sandbox; default-src 'none'; img-src 'self'; style-src 'self';";}
test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java+30 −0
@@ -169,6 +169,36 @@ public void zipDownload() throws Exception {zipfile.delete();}+@Issue("SECURITY-95")+@Test+public void contentSecurityPolicy() throws Exception {+FreeStyleProject p = j.createFreeStyleProject();+p.setScm(new SingleFileSCM("test.html", "<html><body><h1>Hello world!</h1></body></html>"));+p.getPublishersList().add(new ArtifactArchiver("*", "", true));+assertEquals(Result.SUCCESS, p.scheduleBuild2(0).get().getResult());++HtmlPage page = j.createWebClient().goTo("job/" + p.getName() + "/lastSuccessfulBuild/artifact/test.html");+for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {+assertEquals("Header set: " + header, page.getWebResponse().getResponseHeaderValue(header), DirectoryBrowserSupport.DEFAULT_CSP_VALUE);+}++String propName = DirectoryBrowserSupport.class.getName() + ".CSP";+String initialValue = System.getProperty(propName);+try {+System.setProperty(propName, "");+page = j.createWebClient().goTo("job/" + p.getName() + "/lastSuccessfulBuild/artifact/test.html");+for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {+assertFalse("Header not set: " + header, page.getWebResponse().getResponseHeaders().contains(header));+}+} finally {+if (initialValue == null) {+System.clearProperty(DirectoryBrowserSupport.class.getName() + ".CSP");+} else {+System.setProperty(DirectoryBrowserSupport.class.getName() + ".CSP", initialValue);+}+}+}+private File download(UnexpectedPage page) throws IOException {File file = File.createTempFile("DirectoryBrowserSupport", "zipDownload");
[FIX SECURITY-95] Add CSP headers to files served by Jenkins
core/src/main/java/hudson/model/DirectoryBrowserSupport.java+16 −0
@@ -47,6 +47,8 @@import org.apache.commons.io.IOUtils;import org.apache.tools.zip.ZipEntry;import org.apache.tools.zip.ZipOutputStream;+import org.kohsuke.accmod.Restricted;+import org.kohsuke.accmod.restrictions.NoExternalUse;import org.kohsuke.stapler.HttpResponse;import org.kohsuke.stapler.StaplerRequest;import org.kohsuke.stapler.StaplerResponse;@@ -307,6 +309,17 @@ private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root// pseudo file name to let the Stapler set text/plainrsp.serveFile(req, in, lastModified, -1, length, "plain.txt");} else {+String csp = System.getProperty(DirectoryBrowserSupport.class.getName() + ".CSP");+if (csp == null) {+// default value unless overridden with system property+csp = DEFAULT_CSP_VALUE;+}+if (!csp.trim().equals("")) {+// allow users to prevent sending this header by setting empty system property+for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {+rsp.setHeader(header, csp);+}+}rsp.serveFile(req, in, lastModified, -1, length, baseFile.getName() );}}@@ -574,4 +587,7 @@ private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, Lisprivate static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName());++@Restricted(NoExternalUse.class)+public static final String DEFAULT_CSP_VALUE = "sandbox; default-src 'none'; img-src 'self'; style-src 'self';";}
test/src/test/java/hudson/model/DirectoryBrowserSupportTest.java+30 −0
@@ -169,6 +169,36 @@ public void zipDownload() throws Exception {zipfile.delete();}+@Issue("SECURITY-95")+@Test+public void contentSecurityPolicy() throws Exception {+FreeStyleProject p = j.createFreeStyleProject();+p.setScm(new SingleFileSCM("test.html", "<html><body><h1>Hello world!</h1></body></html>"));+p.getPublishersList().add(new ArtifactArchiver("*", "", true));+assertEquals(Result.SUCCESS, p.scheduleBuild2(0).get().getResult());++HtmlPage page = j.createWebClient().goTo("job/" + p.getName() + "/lastSuccessfulBuild/artifact/test.html");+for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {+assertEquals("Header set: " + header, page.getWebResponse().getResponseHeaderValue(header), DirectoryBrowserSupport.DEFAULT_CSP_VALUE);+}++String propName = DirectoryBrowserSupport.class.getName() + ".CSP";+String initialValue = System.getProperty(propName);+try {+System.setProperty(propName, "");+page = j.createWebClient().goTo("job/" + p.getName() + "/lastSuccessfulBuild/artifact/test.html");+for (String header : new String[]{"Content-Security-Policy", "X-WebKit-CSP", "X-Content-Security-Policy"}) {+assertFalse("Header not set: " + header, page.getWebResponse().getResponseHeaders().contains(header));+}+} finally {+if (initialValue == null) {+System.clearProperty(DirectoryBrowserSupport.class.getName() + ".CSP");+} else {+System.setProperty(DirectoryBrowserSupport.class.getName() + ".CSP", initialValue);+}+}+}+private File download(UnexpectedPage page) throws IOException {File file = File.createTempFile("DirectoryBrowserSupport", "zipDownload");
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2015-7536
- WEBhttps://github.com/jenkinsci/jenkins/commit/27c303417a226bf4c06a588570f28ac2e2507c6c
- WEBhttps://github.com/jenkinsci/jenkins/commit/d3fb2c09f29007dce84a213ae8323df1105dcc30
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2015-12-09