mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-10-26 19:15:09 +00:00
Increase minimum required Remoting version from 3176.v207ec082a_8c0 to 3256.3258.v858f3c9a_f69d (#27439)
* Drop support for remoting releases before 2024-08 * Adapt Security3430Test to latest changes Uses the original changes from @daniel-beck, merged with master branch. One relevant change for discussion is the switch from IllegalStateException to IOException in the assertions in the _run method. I believe that is the correct change, but would like validation from @daniel-back to be certain. - assertThat(itex.getCause(), instanceOf(IllegalStateException.class)); + assertThat(itex.getCause(), instanceOf(IOException.class)); Testing done: * Confirmed Security3430Test passes in my development environment --------- Co-authored-by: Daniel Beck <daniel-beck@users.noreply.github.com> Co-authored-by: Daniel Beck <1831569+daniel-beck@users.noreply.github.com>
This commit is contained in:
6 files changed
+26
-263
No files matched your search
+1
-9
@@ -39,7 +39,7 @@ THE SOFTWARE.
|
||||
<properties>
|
||||
<mavenDebug>false</mavenDebug>
|
||||
<!-- Minimum Remoting version, which is tested for API compatibility, duplicated so that renovate only updates the latest remoting version property -->
|
||||
<remoting.minimum.supported.version>3176.v207ec082a_8c0</remoting.minimum.supported.version>
|
||||
<remoting.minimum.supported.version>3256.3258.v858f3c9a_f69d</remoting.minimum.supported.version>
|
||||
<!-- Filled in by jacoco-maven-plugin -->
|
||||
<jacocoSurefireArgs />
|
||||
<!--
|
||||
@@ -371,14 +371,6 @@ THE SOFTWARE.
|
||||
<version>3256.v88a_f6e922152</version>
|
||||
<type>jar</type>
|
||||
<outputDirectory>${project.build.outputDirectory}/old-remoting</outputDirectory>
|
||||
<destFileName>remoting-before-SECURITY-3430-fix.jar</destFileName>
|
||||
</artifactItem>
|
||||
<artifactItem>
|
||||
<groupId>org.jenkins-ci.main</groupId>
|
||||
<artifactId>remoting</artifactId>
|
||||
<version>3085.vc4c6977c075a</version>
|
||||
<type>jar</type>
|
||||
<outputDirectory>${project.build.outputDirectory}/old-remoting</outputDirectory>
|
||||
<destFileName>remoting-unsupported.jar</destFileName>
|
||||
</artifactItem>
|
||||
<artifactItem>
|
||||
|
||||
@@ -2,38 +2,31 @@ package jenkins.security;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeFalse;
|
||||
|
||||
import hudson.ExtensionList;
|
||||
import hudson.Functions;
|
||||
import hudson.model.Computer;
|
||||
import hudson.remoting.Channel;
|
||||
import hudson.remoting.Launcher;
|
||||
import hudson.slaves.SlaveComputer;
|
||||
import hudson.util.RingBufferLogHandler;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
import jenkins.bouncycastle.api.InstallBouncyCastleJCAProvider;
|
||||
import jenkins.security.s2m.JarURLValidatorImpl;
|
||||
import jenkins.slaves.RemotingVersionInfo;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
@@ -46,13 +39,12 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.junit.jupiter.InboundAgentExtension;
|
||||
import org.jvnet.hudson.test.junit.jupiter.RealJenkinsExtension;
|
||||
import org.kohsuke.args4j.Argument;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
|
||||
class Security3430Test {
|
||||
|
||||
@RegisterExtension
|
||||
private final RealJenkinsExtension jj = new RealJenkinsExtension().withLogger(JarURLValidatorImpl.class, Level.FINEST);
|
||||
private final RealJenkinsExtension jj = new RealJenkinsExtension();
|
||||
|
||||
@RegisterExtension
|
||||
private final InboundAgentExtension agents = new InboundAgentExtension();
|
||||
@@ -66,12 +58,6 @@ class Security3430Test {
|
||||
runWithRemoting(RemotingVersionInfo.getMinimumSupportedVersion().toString(), "/old-remoting/remoting-minimum-supported.jar", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithPreviousAgentJar() throws Throwable {
|
||||
assumeFalse(Functions.isWindows() && System.getenv("CI") != null, "Low value, high cost on Windows");
|
||||
runWithRemoting("3256.v88a_f6e922152", "/old-remoting/remoting-before-SECURITY-3430-fix.jar", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithCurrentAgentJar() throws Throwable {
|
||||
assumeFalse(Functions.isWindows() && System.getenv("CI") != null, "Low value, high cost on Windows");
|
||||
@@ -83,18 +69,10 @@ class Security3430Test {
|
||||
final String agentName = "agent1";
|
||||
try {
|
||||
createAgent(agentName, remotingResourcePath);
|
||||
jj.runRemotely(Security3430Test::_run, agentName, expectedRemotingVersion, requestingJarFromAgent, true);
|
||||
jj.runRemotely(Security3430Test::_run, agentName, expectedRemotingVersion, requestingJarFromAgent);
|
||||
} finally {
|
||||
agents.stop(jj, agentName);
|
||||
}
|
||||
jj.runRemotely(Security3430Test::disableJarURLValidatorImpl);
|
||||
final String agentName2 = "agent2";
|
||||
try {
|
||||
createAgent(agentName2, remotingResourcePath);
|
||||
jj.runRemotely(Security3430Test::_run, agentName2, expectedRemotingVersion, requestingJarFromAgent, false);
|
||||
} finally {
|
||||
agents.stop(jj, agentName2);
|
||||
}
|
||||
}
|
||||
|
||||
private void createAgent(String name, String remotingResourcePath) throws Throwable {
|
||||
@@ -115,23 +93,13 @@ class Security3430Test {
|
||||
return ((SlaveComputer) r.jenkins.getComputer(name)).getJnlpMac();
|
||||
}
|
||||
|
||||
// This is quite artificial, but demonstrating that without JarURLValidatorImpl we do not allow any calls from the agent:
|
||||
private static void disableJarURLValidatorImpl(JenkinsRule j) {
|
||||
assertTrue(ExtensionList.lookup(ChannelConfigurator.class).remove(ExtensionList.lookupSingleton(JarURLValidatorImpl.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param agentName the name of the agent we're working with
|
||||
* @param expectedRemotingVersion The version expected for remoting, or {@code null} if we're using whatever is bundled with this Jenkins.
|
||||
* @param requestingJarFromAgent {@code true} if and only if we expect to go through {@code ClassLoaderProxy#fetchJar}
|
||||
* @param hasJenkinsJarURLValidator {@code true} if and only we do not expect {@link jenkins.security.s2m.JarURLValidatorImpl} to be present. Only relevant when {@code requestingJarFromAgent} is {@code true}.
|
||||
*/
|
||||
private static void _run(JenkinsRule j, String agentName, String expectedRemotingVersion, Boolean requestingJarFromAgent, Boolean hasJenkinsJarURLValidator) throws Throwable {
|
||||
final RingBufferLogHandler logHandler = new RingBufferLogHandler(50);
|
||||
Logger.getLogger(JarURLValidatorImpl.class.getName()).addHandler(logHandler);
|
||||
final List<LogRecord> logRecords = logHandler.getView();
|
||||
|
||||
private static void _run(JenkinsRule j, String agentName, String expectedRemotingVersion, Boolean requestingJarFromAgent) throws Throwable {
|
||||
final Computer computer = j.jenkins.getComputer(agentName);
|
||||
assertThat(computer, instanceOf(SlaveComputer.class));
|
||||
SlaveComputer agent = (SlaveComputer) computer;
|
||||
@@ -142,113 +110,37 @@ class Security3430Test {
|
||||
assertThat(result, is(expectedRemotingVersion));
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
|
||||
{ // regular behavior
|
||||
if (hasJenkinsJarURLValidator) {
|
||||
// it works
|
||||
assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class));
|
||||
if (requestingJarFromAgent) {
|
||||
assertThat(logRecords, hasItem(logMessageContainsString("Allowing URL: file:/")));
|
||||
} else {
|
||||
assertThat(logRecords.stream().map(LogRecord::getMessage).toList(), is(empty()));
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
assertFalse(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL"))));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL"))));
|
||||
} else {
|
||||
// outdated remoting.jar will fail, but up to date one passes
|
||||
if (requestingJarFromAgent) {
|
||||
final IOException ex = assertThrows(IOException.class, () -> channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class));
|
||||
assertThat(ex.getMessage(), containsString(
|
||||
"No hudson.remoting.JarURLValidator has been set for this channel, so all #fetchJar calls are rejected. This is likely a bug in Jenkins. As a workaround, try updating the agent.jar file."));
|
||||
} else {
|
||||
assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class));
|
||||
assertThat(logRecords.stream().map(LogRecord::getMessage).toList(), is(empty()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
|
||||
if (hasJenkinsJarURLValidator) { // Start rejecting everything; only applies to JarURLValidatorImpl
|
||||
System.setProperty(JarURLValidatorImpl.class.getName() + ".REJECT_ALL", "true");
|
||||
|
||||
// it works
|
||||
assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class));
|
||||
// Identify that a jar was already loaded:
|
||||
assertFalse(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Stapler.class));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL"))));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL"))));
|
||||
|
||||
logHandler.clear();
|
||||
|
||||
// different jar file than before, old remoting will fail due to call through ClassLoaderProxy#fetchJar, new remoting passes
|
||||
if (requestingJarFromAgent) {
|
||||
final IOException ioException = assertThrows(IOException.class, () -> channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, Argument.class));
|
||||
assertThat(ioException.getMessage(), containsString("all attempts by agents to load jars from the controller are rejected"));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL"))));
|
||||
assertThat(logRecords, hasItem(logMessageContainsString("Rejecting URL due to configuration: ")));
|
||||
} else {
|
||||
assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, org.kohsuke.args4j.Argument.class));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL"))));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL"))));
|
||||
}
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
|
||||
if (hasJenkinsJarURLValidator) { // Disable block, only applies to JarURLValidatorImpl
|
||||
System.clearProperty(JarURLValidatorImpl.class.getName() + ".REJECT_ALL");
|
||||
if (requestingJarFromAgent) {
|
||||
// now it works again for old remoting:
|
||||
assertTrue(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, org.kohsuke.args4j.Argument.class));
|
||||
assertThat(logRecords, hasItem(logMessageContainsString("Allowing URL: file:/")));
|
||||
} else {
|
||||
// new remoting already has it.
|
||||
assertFalse(channel.preloadJar(j.jenkins.getPluginManager().uberClassLoader, org.kohsuke.args4j.Argument.class));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL"))));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL"))));
|
||||
}
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL due to configuration: "))));
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
|
||||
if (hasJenkinsJarURLValidator || !requestingJarFromAgent) { // prepare bouncycastle-api
|
||||
assertTrue(j.jenkins.getPluginManager().getPlugin("bouncycastle-api").isActive());
|
||||
InstallBouncyCastleJCAProvider.on(channel);
|
||||
channel.call(new ConfirmBouncyCastleLibrary());
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
assertTrue(j.jenkins.getPluginManager().getPlugin("bouncycastle-api").isActive());
|
||||
InstallBouncyCastleJCAProvider.on(channel);
|
||||
channel.call(new ConfirmBouncyCastleLibrary());
|
||||
|
||||
{ // Exploitation tests
|
||||
final URL secretKeyFile = new File(j.jenkins.getRootDir(), "secret.key").toURI().toURL();
|
||||
final String expectedContent = IOUtils.toString(secretKeyFile, StandardCharsets.UTF_8);
|
||||
{ // Protection is effective when agents request non-jar files:
|
||||
// Protection is effective when agents request non-jar files:
|
||||
if (expectedRemotingVersion == null) {
|
||||
assertThrows(NoSuchMethodException.class, () -> channel.call(new Exploit(secretKeyFile, expectedContent)));
|
||||
} else {
|
||||
final InvocationTargetException itex = assertThrows(InvocationTargetException.class, () -> channel.call(new Exploit(secretKeyFile, expectedContent)));
|
||||
assertThat(itex.getCause(), instanceOf(IOException.class));
|
||||
if (hasJenkinsJarURLValidator) {
|
||||
assertThat(itex.getCause().getMessage(), containsString("This URL does not point to a jar file allowed to be requested by agents"));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Allowing URL"))));
|
||||
assertThat(logRecords, hasItem(logMessageContainsString("Rejecting URL: ")));
|
||||
} else {
|
||||
assertThat(itex.getCause().getMessage(), containsString(
|
||||
"No hudson.remoting.JarURLValidator has been set for this channel, so all #fetchJar calls are rejected. This is likely a bug in Jenkins. As a workaround, try updating the agent.jar file."));
|
||||
}
|
||||
}
|
||||
|
||||
logHandler.clear();
|
||||
|
||||
{ // Disable protection and non-jar files can be accessed:
|
||||
System.setProperty(Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR", "true");
|
||||
channel.call(new Exploit(secretKeyFile, expectedContent));
|
||||
if (hasJenkinsJarURLValidator) {
|
||||
assertThat(logRecords, hasItem(logMessageContainsString("Allowing URL due to configuration")));
|
||||
assertThat(logRecords, not(hasItem(logMessageContainsString("Rejecting URL"))));
|
||||
}
|
||||
System.clearProperty(Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR");
|
||||
}
|
||||
{ // Support for pre-2024-08 remoting has been dropped, so even formerly legitimate jar files cannot be requested anymore:
|
||||
final URLClassLoader classLoader = (URLClassLoader) j.jenkins.getPluginManager().getPlugin("bouncycastle-api").classLoader;
|
||||
URL safeUrl = classLoader.getURLs()[0];
|
||||
final String expectedContent = IOUtils.toString(safeUrl, StandardCharsets.UTF_8);
|
||||
if (expectedRemotingVersion == null) {
|
||||
assertThrows(NoSuchMethodException.class, () -> channel.call(new Exploit(safeUrl, expectedContent)));
|
||||
} else {
|
||||
final InvocationTargetException itex = assertThrows(InvocationTargetException.class, () -> channel.call(new Exploit(safeUrl, expectedContent)));
|
||||
assertThat(itex.getCause(), instanceOf(IOException.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user