diff --git a/core/src/main/java/hudson/model/labels/LabelAtom.java b/core/src/main/java/hudson/model/labels/LabelAtom.java index 16ec68c4350e09c7ae4e65539b4ba451027e1bd7..ccd7e5997cbbc8efc469a9b0b822c92ca56c1c7a 100644 --- a/core/src/main/java/hudson/model/labels/LabelAtom.java +++ b/core/src/main/java/hudson/model/labels/LabelAtom.java @@ -33,13 +33,16 @@ import hudson.XmlFile; import hudson.model.Action; import hudson.model.Descriptor.FormException; import hudson.model.Failure; +import hudson.model.FileParameterValue; import hudson.util.*; import jenkins.model.Jenkins; import hudson.model.Label; import hudson.model.Saveable; import hudson.model.listeners.SaveableListener; +import jenkins.util.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; @@ -56,6 +59,8 @@ import java.util.Set; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; + import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -66,6 +71,12 @@ import edu.umd.cs.findbugs.annotations.Nullable; * @since 1.372 */ public class LabelAtom extends Label implements Saveable { + + private static final Pattern PROHIBITED_DOUBLE_DOT = Pattern.compile(".*\\.\\.[\\\\/].*"); + + private static /* Script Console modifiable */ boolean ALLOW_FOLDER_TRAVERSAL = + SystemProperties.getBoolean(LabelAtom.class.getName() + ".allowFolderTraversal"); + private DescribableList properties = new DescribableList<>(this); @@ -167,6 +178,9 @@ public class LabelAtom extends Label implements Saveable { } public void save() throws IOException { + if (isInvalidName()) { + throw new IOException("Invalid label"); + } if(BulkChange.contains(this)) return; try { getConfigFile().write(this); @@ -206,6 +220,10 @@ public class LabelAtom extends Label implements Saveable { app.checkPermission(Jenkins.ADMINISTER); + if (isInvalidName()) { + throw new FormException("Invalid label", null); + } + properties.rebuild(req, req.getSubmittedForm(), getApplicablePropertyDescriptors()); this.description = req.getSubmittedForm().getString("description"); @@ -216,6 +234,10 @@ public class LabelAtom extends Label implements Saveable { FormApply.success(".").generateResponse(req, rsp, null); } + private boolean isInvalidName() { + return !ALLOW_FOLDER_TRAVERSAL && PROHIBITED_DOUBLE_DOT.matcher(name).matches(); + } + /** * Accepts the new description. */ diff --git a/test/src/test/java/hudson/model/labels/LabelAtomSecurity1986Test.java b/test/src/test/java/hudson/model/labels/LabelAtomSecurity1986Test.java new file mode 100644 index 0000000000000000000000000000000000000000..78299238237646cccacb8b63c1a54542b21263e5 --- /dev/null +++ b/test/src/test/java/hudson/model/labels/LabelAtomSecurity1986Test.java @@ -0,0 +1,207 @@ +/* + * The MIT License + * + * Copyright (c) 2020, 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 hudson.model.labels; + +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import hudson.XmlFile; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class LabelAtomSecurity1986Test { + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Test + public void nonexisting() throws Exception { + LabelAtom nonexistent = j.jenkins.getLabelAtom("nonexistent"); + XmlFile configFile = nonexistent.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + + @Test + public void normal() throws Exception { + j.submit(j.createWebClient().goTo("labelAtom/foo/configure").getFormByName("config")); + LabelAtom foo = j.jenkins.getLabelAtom("foo"); + XmlFile configFile = foo.getConfigFile(); + assertTrue(configFile.getFile().exists()); + assertThat(configFile.getFile().getParentFile().getName(), equalTo("labels")); + } + + @Test + @Issue("SECURITY-1986") + public void startsWithDoubleDotSlash() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/..%2ffoo/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom("../foo"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + + @Test + public void startsWithSlash() throws Exception { + // Not a great result, but it works and doesn't cause problems. + j.submit(j.createWebClient().goTo("labelAtom/%2ffoo/configure").getFormByName("config")); + LabelAtom foo = j.jenkins.getLabelAtom("/foo"); + XmlFile configFile = foo.getConfigFile(); + assertTrue(configFile.getFile().exists()); + assertThat(configFile.getFile().getParentFile().getName(), equalTo("labels")); + } + + @Test + public void startsWithDoubleDot() throws Exception { + j.submit(j.createWebClient().goTo("labelAtom/..foo/configure").getFormByName("config")); + LabelAtom foo = j.jenkins.getLabelAtom("..foo"); + XmlFile configFile = foo.getConfigFile(); + assertTrue(configFile.getFile().exists()); + assertThat(configFile.getFile().getParentFile().getName(), equalTo("labels")); + } + + @Test + @Issue("SECURITY-1986") + public void endsWithDoubleDotSlash() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/foo..%2f/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom("foo../"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + + @Test + public void endsWithDoubleDot() throws Exception { + j.submit(j.createWebClient().goTo("labelAtom/foo../configure").getFormByName("config")); + LabelAtom foo = j.jenkins.getLabelAtom("foo.."); + XmlFile configFile = foo.getConfigFile(); + assertTrue(configFile.getFile().exists()); + assertThat(configFile.getFile().getParentFile().getName(), equalTo("labels")); + } + + @Test + @Issue("SECURITY-1986") + public void startsWithDoubleDotBackslash() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/..\\foo/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom("..\\foo"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + + @Test + @Issue("SECURITY-1986") + public void endsWithDoubleDotBackslash() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/foo..\\/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom("foo..\\"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + + @Test + @Issue("SECURITY-1986") + public void middleDotsSlashes() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/foo%2f..%2fgoo/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom("foo/../goo"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + + @Test + @Issue("SECURITY-1986") + public void middleDotsBackslashes() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/foo%\\..\\goo/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom("foo\\..\\"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + + @Test(expected = Exception.class) + @Issue("SECURITY-1986") + public void programmaticCreationInvalidName() throws IOException { + LabelAtom label = new LabelAtom("foo/../goo"); + label.save(); + } + + @Test + public void programmaticCreation() throws IOException { + LabelAtom label = new LabelAtom("foo"); + label.save(); + LabelAtom foo = j.jenkins.getLabelAtom("foo"); + XmlFile configFile = foo.getConfigFile(); + assertTrue(configFile.getFile().exists()); + assertThat(configFile.getFile().getParentFile().getName(), equalTo("labels")); + } + + @Test + @Issue("SECURITY-1986") + public void startsWithTripleDotBackslash() throws Exception { + try { + j.submit(j.createWebClient().goTo("labelAtom/...%2ffoo/configure").getFormByName("config")); + fail("Should have rejected label."); + } catch (FailingHttpStatusCodeException e) { + assertThat(e.getStatusCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + LabelAtom foo = j.jenkins.getLabelAtom(".../foo"); + XmlFile configFile = foo.getConfigFile(); + assertFalse(configFile.getFile().exists()); + } + } + +}