diff --git a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java index ef4955213db7..c591827ec8ee 100644 --- a/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java +++ b/core/src/main/java/jenkins/security/s2m/DefaultFilePathFilter.java @@ -26,12 +26,17 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.ExtensionList; import hudson.remoting.ChannelBuilder; +import hudson.remoting.Command; +import hudson.remoting.Request; import java.io.File; +import java.lang.reflect.Field; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.ReflectiveFilePathFilter; import jenkins.security.ChannelConfigurator; +import jenkins.telemetry.impl.SlaveToMasterFileCallableUsage; import jenkins.util.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -59,6 +64,17 @@ protected boolean op(String op, File f) throws SecurityException { LOGGER.log(Level.FINE, "agent allowed to {0} {1}", new Object[] {op, f}); return true; } else { + try { + Field current = Request.class.getDeclaredField("CURRENT"); + current.setAccessible(true); + Field createdAt = Command.class.getDeclaredField("createdAt"); + createdAt.setAccessible(true); + Throwable trace = (Throwable) createdAt.get(((ThreadLocal) current.get(null)).get()); + ExtensionList.lookupSingleton(SlaveToMasterFileCallableUsage.class).recordTrace(trace); + LOGGER.log(Level.WARNING, "Permitting agent-to-controller '" + op + "' on '" + f + "'. This is deprecated and will soon be rejected. Learn more: https://www.jenkins.io/redirect/permitted-agent-to-controller-file-access", trace); + } catch (Exception x) { + LOGGER.log(Level.WARNING, null, x); + } return false; } } diff --git a/core/src/main/java/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage.java b/core/src/main/java/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage.java new file mode 100644 index 000000000000..dd628b767773 --- /dev/null +++ b/core/src/main/java/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage.java @@ -0,0 +1,75 @@ +/* + * The MIT License + * + * Copyright 2021 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.telemetry.impl; + +import hudson.Extension; +import hudson.Functions; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import jenkins.SlaveToMasterFileCallable; +import jenkins.security.s2m.DefaultFilePathFilter; +import jenkins.telemetry.Telemetry; +import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Records when {@link DefaultFilePathFilter} found {@link SlaveToMasterFileCallable} or similar being used. + */ +@Extension +@Restricted(NoExternalUse.class) +public class SlaveToMasterFileCallableUsage extends Telemetry { + + private Set traces = new TreeSet<>(); + + @Override + public String getDisplayName() { + return "Access to files on controllers from code running on an agent"; + } + + @Override + public LocalDate getStart() { + return LocalDate.of(2021, 11, 4); // https://www.jenkins.io/security/advisory/2021-11-04/ + } + + @Override + public LocalDate getEnd() { + return LocalDate.of(2022, 3, 1); + } + + @Override + public synchronized JSONObject createContent() { + JSONObject json = JSONObject.fromObject(Collections.singletonMap("traces", traces)); + traces.clear(); + return json; + } + + public synchronized void recordTrace(Throwable trace) { + traces.add(Functions.printThrowable(trace).replaceAll("@[a-f0-9]+", "@…")); + } + +} diff --git a/core/src/main/resources/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage/description.jelly b/core/src/main/resources/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage/description.jelly new file mode 100644 index 000000000000..83f02a200bf1 --- /dev/null +++ b/core/src/main/resources/jenkins/telemetry/impl/SlaveToMasterFileCallableUsage/description.jelly @@ -0,0 +1,13 @@ + + + Jenkins controllers construct a remote-procedure-call (RPC) channel to agents to instruct them to perform work. + This channel is bidirectional and in a handful of cases agents made requests of the controller. + This was always tricky to secure, + and recently + the category of usages which involved access to files was more tightly restricted than before; + Jenkins developers are considering disabling this kind of usage entirely. + Since it is difficult to determine via static analysis or even manual code inspection which plugins are using this system, + we are collecting information on how widely it is used. + The data includes names of Java classes mainly in Jenkins core and plugins as well as method names and line numbers. + It does not include the names of files being accessed or anything else not determined by versions of software components in use. + diff --git a/test/src/test/java/jenkins/security/s2m/AdminFilePathFilterTest.java b/test/src/test/java/jenkins/security/s2m/AdminFilePathFilterTest.java index 4c4d8f41a29a..383417a2db9e 100644 --- a/test/src/test/java/jenkins/security/s2m/AdminFilePathFilterTest.java +++ b/test/src/test/java/jenkins/security/s2m/AdminFilePathFilterTest.java @@ -25,13 +25,17 @@ package jenkins.security.s2m; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.jvnet.hudson.test.LoggerRule.recorded; +import hudson.ExtensionList; import hudson.FilePath; import hudson.model.Slave; import hudson.remoting.Callable; @@ -40,9 +44,11 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; +import java.util.List; import java.util.logging.Level; import javax.inject.Inject; import jenkins.SoloFilePathFilter; +import jenkins.telemetry.impl.SlaveToMasterFileCallableUsage; import org.jenkinsci.remoting.RoleChecker; import org.junit.Before; import org.junit.Rule; @@ -107,6 +113,14 @@ public void slaveCannotReadFileFromSecrets_butCanFromUserContent() throws Except checkSlave_can_readFile(s, rootTargetPrivate); } + + @SuppressWarnings("unchecked") + List traces = (List) ExtensionList.lookupSingleton(SlaveToMasterFileCallableUsage.class).createContent().getJSONArray("traces"); + assertThat(traces, hasSize(1)); + assertThat(traces.get(0), allOf(containsString("Command UserRequest:hudson.FilePath$ReadToString@… created at"), containsString(ReadFileS2MCallable.class.getName() + ".call"))); + @SuppressWarnings("unchecked") + List cleared = (List) ExtensionList.lookupSingleton(SlaveToMasterFileCallableUsage.class).createContent().getJSONArray("traces"); + assertThat(cleared, empty()); } private static class ReadFileS2MCallable implements Callable {