Security context
Medium· 6.3GHSA-6q4g-84f3-mw74 CVE-2021-21682CWE-42Published May 24, 2022

Improper handling of equivalent directory names on Windows in Jenkins

Research this vulnerability

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

2.304 → fixed in 2.3150 → fixed in 2.303.2

Details

Jenkins stores jobs and other entities on disk using their name shown on the UI as file and folder names. On Windows, when specifying a file or folder with a trailing dot character (`example.`), the file or folder will be treated as if that character was not present (`example`). As both are legal names for jobs and other entities in Jenkins 2.314 and earlier, LTS 2.303.1 and earlier, this could allow users with the appropriate permissions to change or replace configurations of jobs and other entities. Jenkins 2.315, LTS 2.303.2 does not allow names of jobs and other entities to end with a dot character.

The fix

[SECURITY-2424]

Wadeck· Sep 22, 2021, 04:15 PM+5970c2c2b59071
core/src/main/java/jenkins/model/Jenkins.java+20 0
@@ -4161,6 +4161,13 @@ public static void checkGoodName(String name) throws Failure {
throw new Failure(Messages.Hudson_UnsafeChar(ch));
}
+ if (SystemProperties.getBoolean(NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP, true)) {
+ // SECURITY-2424 on Windows the trailing dot can be used to create ambiguity
+ if (name.trim().endsWith(".")) {
+ throw new Failure(Messages.Hudson_TrailingDot());
+ }
+ }
+
// looks good
}
@@ -5413,6 +5420,19 @@ public boolean shouldShowStackTrace() {
*/
private static final String WORKSPACE_DIRNAME = SystemProperties.getString(Jenkins.class.getName() + "." + "workspaceDirName", "workspace");
+ /**
+ * Name of the system property escape hatch for SECURITY-2424. It allows to have back the legacy (and vulnerable)
+ * behavior allowing a "good name" to end with a dot. This could be used to exploit two names colliding in the file
+ * system to extract information. The files ending with a dot are only a problem on Windows.
+ *
+ * The default value is true.
+ *
+ * For detailed documentation: https://docs.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/file-folder-name-whitespace-characters
+ * @see #checkGoodName(String)
+ */
+ @Restricted(NoExternalUse.class)
+ public static final String NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP = Jenkins.class.getName() + "." + "nameValidationRejectsTrailingDot";
+
/**
* Default value of job's builds dir.
* @see #getRawBuildsDir()
core/src/main/resources/hudson/model/Messages.properties+1 0
@@ -139,6 +139,7 @@ Hudson.NotJDKDir={0} doesn\u2019t look like a JDK directory
Hudson.Permissions.Title=Overall
Hudson.USER_CONTENT_README=Files in this directory will be served under your http://yourjenkins/userContent/
Hudson.UnsafeChar=\u2018{0}\u2019 is an unsafe character
+Hudson.TrailingDot=A name cannot end with \u2018.\u2019
Hudson.ViewAlreadyExists=A view already exists with the name "{0}"
Hudson.ViewName=All
Hudson.NotANumber=Not a number
core/src/test/java/jenkins/model/JenkinsSEC2424Test.java+73 0
@@ -0,0 +1,73 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2014
+ *
+ * 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.model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import hudson.model.Failure;
+import hudson.model.Messages;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+
+public class JenkinsSEC2424Test {
+ @Test
+ @Issue("SECURITY-2424")
+ public void doesNotAcceptNameWithTrailingDot_regular() {
+ try {
+ Jenkins.checkGoodName("job.");
+ fail("Names with dot should not be accepted");
+ } catch (Failure e) {
+ assertEquals(Messages.Hudson_TrailingDot(), e.getMessage());
+ }
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void doesNotAcceptNameWithTrailingDot_withSpaces() {
+ try {
+ Jenkins.checkGoodName("job. ");
+ fail("Names with dot should not be accepted");
+ } catch (Failure e) {
+ assertEquals(Messages.Hudson_TrailingDot(), e.getMessage());
+ }
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void doesNotAcceptNameWithTrailingDot_exceptIfEscapeHatchIsSet() {
+ String propName = Jenkins.NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP;
+ String initialValue = System.getProperty(propName);
+ System.setProperty(propName, "false");
+ try {
+ Jenkins.checkGoodName("job.");
+ } finally {
+ if (initialValue == null) {
+ System.clearProperty(propName);
+ } else {
+ System.setProperty(propName, initialValue);
+ }
+ }
+ }
+}
test/src/test/java/hudson/cli/CopyJobCommandSEC2424Test.java+89 0
@@ -0,0 +1,89 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2012 Jesse Glick.
+ *
+ * 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.cli;
+
+import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
+import static hudson.cli.CLICommandInvoker.Matcher.succeededSilently;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+import hudson.model.Messages;
+import jenkins.model.Jenkins;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+//TODO merge back to CopyJobCommandTest after security release
+public class CopyJobCommandSEC2424Test {
+
+ @Rule public JenkinsRule j = new JenkinsRule();
+ private CLICommand copyJobCommand;
+ private CLICommandInvoker command;
+
+ @Before public void setUp() {
+ copyJobCommand = new CopyJobCommand();
+ command = new CLICommandInvoker(j, copyJobCommand);
+ }
+
+ @Issue("SECURITY-2424")
+ @Test public void cannotCopyJobWithTrailingDot_regular() throws Exception {
+ assertThat(j.jenkins.getItems(), Matchers.hasSize(0));
+ j.createFreeStyleProject("job1");
+ assertThat(j.jenkins.getItems(), Matchers.hasSize(1));
+
+ CLICommandInvoker.Result result = command.invokeWithArgs("job1", "job1.");
+ assertThat(result.stderr(), containsString(Messages.Hudson_TrailingDot()));
+ assertThat(result, failedWith(1));
+
+ assertThat(j.jenkins.getItems(), Matchers.hasSize(1));
+ }
+
+ @Issue("SECURITY-2424")
+ @Test public void cannotCopyJobWithTrailingDot_exceptIfEscapeHatchIsSet() throws Exception {
+ String propName = Jenkins.NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP;
+ String initialValue = System.getProperty(propName);
+ System.setProperty(propName, "false");
+ try {
+ assertThat(j.jenkins.getItems(), Matchers.hasSize(0));
+ j.createFreeStyleProject("job1");
+ assertThat(j.jenkins.getItems(), Matchers.hasSize(1));
+
+ CLICommandInvoker.Result result = command.invokeWithArgs("job1", "job1.");
+ assertThat(result, succeededSilently());
+
+ assertThat(j.jenkins.getItems(), Matchers.hasSize(2));
+ }
+ finally {
+ if (initialValue == null) {
+ System.clearProperty(propName);
+ } else {
+ System.setProperty(propName, initialValue);
+ }
+ }
+ }
+}
test/src/test/java/hudson/cli/CreateJobCommandSEC2424Test.java+96 0
@@ -0,0 +1,96 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2014 Jesse Glick.
+ *
+ * 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.cli;
+
+import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
+import static hudson.cli.CLICommandInvoker.Matcher.succeededSilently;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+import hudson.model.Messages;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import jenkins.model.Jenkins;
+import org.hamcrest.Matchers;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+
+//TODO merge back to CreateJobCommandTest after security release
+public class CreateJobCommandSEC2424Test {
+
+ @Rule public JenkinsRule r = new JenkinsRule();
+
+ @Issue("SECURITY-2424")
+ @Test public void cannotCreateJobWithTrailingDot_withoutOtherJob() {
+ CLICommand cmd = new CreateJobCommand();
+ CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(0));
+
+ CLICommandInvoker.Result result = invoker.withStdin(new ByteArrayInputStream("<project/>".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job1.");
+ assertThat(result.stderr(), containsString(Messages.Hudson_TrailingDot()));
+ assertThat(result, failedWith(1));
+
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(0));
+ }
+
+ @Issue("SECURITY-2424")
+ @Test public void cannotCreateJobWithTrailingDot_withExistingJob() {
+ CLICommand cmd = new CreateJobCommand();
+ CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(0));
+ assertThat(invoker.withStdin(new ByteArrayInputStream("<project/>".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job1"), succeededSilently());
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(1));
+
+ CLICommandInvoker.Result result = invoker.withStdin(new ByteArrayInputStream("<project/>".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job1.");
+ assertThat(result.stderr(), containsString(Messages.Hudson_TrailingDot()));
+ assertThat(result, failedWith(1));
+
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(1));
+ }
+
+ @Issue("SECURITY-2424")
+ @Test public void cannotCreateJobWithTrailingDot_exceptIfEscapeHatchIsSet() {
+ String propName = Jenkins.NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP;
+ String initialValue = System.getProperty(propName);
+ System.setProperty(propName, "false");
+ try {
+ CLICommand cmd = new CreateJobCommand();
+ CLICommandInvoker invoker = new CLICommandInvoker(r, cmd);
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(0));
+ assertThat(invoker.withStdin(new ByteArrayInputStream("<project/>".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("job1."), succeededSilently());
+ assertThat(r.jenkins.getItems(), Matchers.hasSize(1));
+ }
+ finally {
+ if (initialValue == null) {
+ System.clearProperty(propName);
+ } else {
+ System.setProperty(propName, initialValue);
+ }
+ }
+ }
+}
test/src/test/java/hudson/cli/CreateNodeCommandSEC2424Test.java+125 0
@@ -0,0 +1,125 @@
+/*
+ * The MIT License
+ *
+ * Copyright 2013 Red Hat, 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.cli;
+
+import static hudson.cli.CLICommandInvoker.Matcher.failedWith;
+import static hudson.cli.CLICommandInvoker.Matcher.hasNoStandardOutput;
+import static hudson.cli.CLICommandInvoker.Matcher.succeededSilently;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+
+import hudson.model.Messages;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+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;
+
+
+//TODO merge back to CreateNodeCommandTest after security release
+public class CreateNodeCommandSEC2424Test {
+
+ private CLICommandInvoker command;
+
+ @Rule public final JenkinsRule j = new JenkinsRule();
+
+ @Before public void setUp() {
+
+ command = new CLICommandInvoker(j, new CreateNodeCommand());
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateNodeWithTrailingDot_withoutOtherNode() {
+ int nodeListSizeBefore = j.jenkins.getNodes().size();
+
+ CLICommandInvoker.Result result = command
+ .withStdin(new ByteArrayInputStream("<slave/>".getBytes(StandardCharsets.UTF_8)))
+ .invokeWithArgs("nodeA.")
+ ;
+
+ assertThat(result.stderr(), containsString(Messages.Hudson_TrailingDot()));
+ assertThat(result, hasNoStandardOutput());
+ assertThat(result, failedWith(1));
+
+ // ensure not side effects
+ assertEquals(nodeListSizeBefore, j.jenkins.getNodes().size());
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateNodeWithTrailingDot_withExistingNode() {
+ int nodeListSizeBefore = j.jenkins.getNodes().size();
+
+ assertThat(command.withStdin(new ByteArrayInputStream("<slave/>".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("nodeA"), succeededSilently());
+ assertEquals(nodeListSizeBefore + 1, j.jenkins.getNodes().size());
+
+ CLICommandInvoker.Result result = command
+ .withStdin(new ByteArrayInputStream("<slave/>".getBytes(StandardCharsets.UTF_8)))
+ .invokeWithArgs("nodeA.")
+ ;
+
+ assertThat(result.stderr(), containsString(Messages.Hudson_TrailingDot()));
+ assertThat(result, hasNoStandardOutput());
+ assertThat(result, failedWith(1));
+
+ // ensure not side effects
+ assertEquals(nodeListSizeBefore + 1, j.jenkins.getNodes().size());
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateNodeWithTrailingDot_exceptIfEscapeHatchIsSet() {
+ String propName = Jenkins.NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP;
+ String initialValue = System.getProperty(propName);
+ System.setProperty(propName, "false");
+ try {
+ int nodeListSizeBefore = j.jenkins.getNodes().size();
+
+ assertThat(command.withStdin(new ByteArrayInputStream("<slave/>".getBytes(StandardCharsets.UTF_8))).invokeWithArgs("nodeA"), succeededSilently());
+ assertEquals(nodeListSizeBefore + 1, j.jenkins.getNodes().size());
+
+ CLICommandInvoker.Result result = command
+ .withStdin(new ByteArrayInputStream("<slave/>".getBytes(StandardCharsets.UTF_8)))
+ .invokeWithArgs("nodeA.")
+ ;
+
+ assertThat(result, succeededSilently());
+
+ assertEquals(nodeListSizeBefore + 2, j.jenkins.getNodes().size());
+ }
+ finally {
+ if (initialValue == null) {
+ System.clearProperty(propName);
+ } else {
+ System.setProperty(propName, initialValue);
+ }
+ }
+ }
+}
test/src/test/java/hudson/model/FreeStyleProjectSEC2424Test.java+92 0
@@ -0,0 +1,92 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
+ *
+ * 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;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import jenkins.model.Jenkins;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+//TODO merge back to FreeStyleProjectTest after security release
+public class FreeStyleProjectSEC2424Test {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateJobWithTrailingDot_withoutOtherJob() throws Exception {
+ assertThat(j.jenkins.getItems(), hasSize(0));
+ try {
+ j.jenkins.createProjectFromXML("jobA.", new ByteArrayInputStream("<project/>".getBytes()));
+ fail("Adding the job should have thrown an exception during checkGoodName");
+ }
+ catch (Failure e) {
+ assertEquals(Messages.Hudson_TrailingDot(), e.getMessage());
+ }
+ assertThat(j.jenkins.getItems(), hasSize(0));
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateJobWithTrailingDot_withExistingJob() throws Exception {
+ assertThat(j.jenkins.getItems(), hasSize(0));
+ j.createFreeStyleProject("jobA");
+ assertThat(j.jenkins.getItems(), hasSize(1));
+ try {
+ j.jenkins.createProjectFromXML("jobA.", new ByteArrayInputStream("<project/>".getBytes()));
+ fail("Adding the job should have thrown an exception during checkGoodName");
+ }
+ catch (Failure e) {
+ assertEquals(Messages.Hudson_TrailingDot(), e.getMessage());
+ }
+ assertThat(j.jenkins.getItems(), hasSize(1));
+ }
+
+ @Issue("SECURITY-2424")
+ @Test public void cannotCreateJobWithTrailingDot_exceptIfEscapeHatchIsSet() throws Exception {
+ String propName = Jenkins.NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP;
+ String initialValue = System.getProperty(propName);
+ System.setProperty(propName, "false");
+ try {
+ assertThat(j.jenkins.getItems(), hasSize(0));
+ j.jenkins.createProjectFromXML("jobA.", new ByteArrayInputStream("<project/>".getBytes()));
+ }
+ finally {
+ if (initialValue == null) {
+ System.clearProperty(propName);
+ } else {
+ System.setProperty(propName, initialValue);
+ }
+ }
+ assertThat(j.jenkins.getItems(), hasSize(1));
+ }
+}
test/src/test/java/jenkins/model/NodesSEC2424Test.java+101 0
@@ -0,0 +1,101 @@
+/*
+ * The MIT License
+ *
+ * Copyright 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.model;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import hudson.model.Failure;
+import hudson.model.Messages;
+import hudson.slaves.DumbSlave;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+
+//TODO merge back to NodesTest after security release
+public class NodesSEC2424Test {
+
+ @Rule
+ public JenkinsRule r = new JenkinsRule();
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateNodeWithTrailingDot_withoutOtherNode() throws Exception {
+ assertThat(r.jenkins.getNodes(), hasSize(0));
+
+ DumbSlave node = new DumbSlave("nodeA.", "temp", r.createComputerLauncher(null));
+ try {
+ r.jenkins.addNode(node);
+ fail("Adding the node should have thrown an exception during checkGoodName");
+ } catch (Failure e) {
+ assertEquals(Messages.Hudson_TrailingDot(), e.getMessage());
+ }
+
+ assertThat(r.jenkins.getNodes(), hasSize(0));
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateNodeWithTrailingDot_withExistingNode() throws Exception {
+ assertThat(r.jenkins.getNodes(), hasSize(0));
+ r.createSlave("nodeA", "", null);
+ assertThat(r.jenkins.getNodes(), hasSize(1));
+
+ DumbSlave node = new DumbSlave("nodeA.", "temp", r.createComputerLauncher(null));
+ try {
+ r.jenkins.addNode(node);
+ fail("Adding the node should have thrown an exception during checkGoodName");
+ } catch (Failure e) {
+ assertEquals(Messages.Hudson_TrailingDot(), e.getMessage());
+ }
+
+ assertThat(r.jenkins.getNodes(), hasSize(1));
+ }
+
+ @Test
+ @Issue("SECURITY-2424")
+ public void cannotCreateNodeWithTrailingDot_exceptIfEscapeHatchIsSet() throws Exception {
+ String propName = Jenkins.NAME_VALIDATION_REJECTS_TRAILING_DOT_PROP;
+ String initialValue = System.getProperty(propName);
+ System.setProperty(propName, "false");
+ try {
+ assertThat(r.jenkins.getNodes(), hasSize(0));
+
+ DumbSlave node = new DumbSlave("nodeA.", "temp", r.createComputerLauncher(null));
+ r.jenkins.addNode(node);
+
+ assertThat(r.jenkins.getNodes(), hasSize(1));
+ } finally {
+ if (initialValue == null) {
+ System.clearProperty(propName);
+ } else {
+ System.setProperty(propName, initialValue);
+ }
+ }
+ }
+}

References