From: wolframhaussig <13997737+wolframhaussig@users.noreply.github.com> Date: Sun, 13 Jun 2021 09:10:35 +0000 (+0200) Subject: Add secure wrapper to plugin manager (#450) X-Git-Tag: release-3.7.0~14 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=dac4edb5f3fbfd5e5aae81ae4eb3c4dad2e7f732;p=pf4j.git Add secure wrapper to plugin manager (#450) --- diff --git a/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java b/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java index 9562257..2374b56 100644 --- a/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java +++ b/pf4j/src/main/java/org/pf4j/AbstractPluginManager.java @@ -860,10 +860,7 @@ public abstract class AbstractPluginManager implements PluginManager { ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor); log.debug("Loaded plugin '{}' with class loader '{}'", pluginPath, pluginClassLoader); - // create the plugin wrapper - log.debug("Creating wrapper for plugin '{}'", pluginPath); - PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); - pluginWrapper.setPluginFactory(getPluginFactory()); + PluginWrapper pluginWrapper = createPluginWrapper(pluginDescriptor, pluginPath, pluginClassLoader); // test for disabled plugin if (isPluginDisabled(pluginDescriptor.getPluginId())) { @@ -890,6 +887,19 @@ public abstract class AbstractPluginManager implements PluginManager { return pluginWrapper; } + + /** + * creates the plugin wrapper. override this if you want to prevent plugins having full access to the plugin manager + * + * @return + */ + protected PluginWrapper createPluginWrapper(PluginDescriptor pluginDescriptor, Path pluginPath, ClassLoader pluginClassLoader) { + // create the plugin wrapper + log.debug("Creating wrapper for plugin '{}'", pluginPath); + PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); + pluginWrapper.setPluginFactory(getPluginFactory()); + return pluginWrapper; + } /** * Tests for already loaded plugins on given path. diff --git a/pf4j/src/main/java/org/pf4j/PluginClassLoader.java b/pf4j/src/main/java/org/pf4j/PluginClassLoader.java index 4dbcb6a..d22dd55 100644 --- a/pf4j/src/main/java/org/pf4j/PluginClassLoader.java +++ b/pf4j/src/main/java/org/pf4j/PluginClassLoader.java @@ -103,7 +103,7 @@ public class PluginClassLoader extends URLClassLoader { } // if the class is part of the plugin engine use parent class loader - if (className.startsWith(PLUGIN_PACKAGE_PREFIX) && !className.startsWith("org.pf4j.demo")) { + if (className.startsWith(PLUGIN_PACKAGE_PREFIX) && !className.startsWith("org.pf4j.demo") && !className.startsWith("org.pf4j.test")) { // log.trace("Delegate the loading of PF4J class '{}' to parent", className); return getParent().loadClass(className); } diff --git a/pf4j/src/main/java/org/pf4j/SecurePluginManagerWrapper.java b/pf4j/src/main/java/org/pf4j/SecurePluginManagerWrapper.java new file mode 100644 index 0000000..f55b1e6 --- /dev/null +++ b/pf4j/src/main/java/org/pf4j/SecurePluginManagerWrapper.java @@ -0,0 +1,305 @@ +package org.pf4j; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Use this class to wrap the original plugin manager to prevent full access from within plugins. + * Override AbstractPluginManager.createPluginWrapper to use this class + * @author Wolfram Haussig + * + */ +public class SecurePluginManagerWrapper implements PluginManager { + + private static final String PLUGIN_PREFIX = "Plugin "; + /** + * the current plugin + */ + private String currentPluginId; + /** + * the original plugin manager + */ + private PluginManager original; + + /** + * The registered {@link PluginStateListener}s. + */ + protected List pluginStateListeners = new ArrayList<>(); + /** + * wrapper for pluginStateListeners + */ + private PluginStateListenerWrapper listenerWrapper = new PluginStateListenerWrapper(); + + /** + * constructor + * @param original the original plugin manager + * @param currentPlugin the current pluginId + */ + public SecurePluginManagerWrapper(PluginManager original, String currentPluginId) { + this.original = original; + this.currentPluginId = currentPluginId; + } + + @Override + public boolean isDevelopment() { + return original.isDevelopment(); + } + + @Override + public boolean isNotDevelopment() { + return original.isNotDevelopment(); + } + + @Override + public List getPlugins() { + return Arrays.asList(getPlugin(currentPluginId)); + } + + @Override + public List getPlugins(PluginState pluginState) { + return getPlugins().stream().filter(p -> p.getPluginState() == pluginState).collect(Collectors.toList()); + } + + @Override + public List getResolvedPlugins() { + return getPlugins().stream().filter(p -> p.getPluginState().ordinal() >= PluginState.RESOLVED.ordinal()).collect(Collectors.toList()); + } + + @Override + public List getUnresolvedPlugins() { + return Collections.emptyList(); + } + + @Override + public List getStartedPlugins() { + return getPlugins(PluginState.STARTED); + } + + @Override + public PluginWrapper getPlugin(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getPlugin(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getPlugin for foreign pluginId!"); + } + } + + @Override + public void loadPlugins() { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute loadPlugins!"); + } + + @Override + public String loadPlugin(Path pluginPath) { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute loadPlugin!"); + } + + @Override + public void startPlugins() { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute startPlugins!"); + } + + @Override + public PluginState startPlugin(String pluginId) { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute startPlugin!"); + } + + @Override + public void stopPlugins() { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute stopPlugins!"); + } + + @Override + public PluginState stopPlugin(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.stopPlugin(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute stopPlugin for foreign pluginId!"); + } + } + + @Override + public void unloadPlugins() { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute unloadPlugins!"); + } + + @Override + public boolean unloadPlugin(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.unloadPlugin(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute unloadPlugin for foreign pluginId!"); + } + } + + @Override + public boolean disablePlugin(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.disablePlugin(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute disablePlugin for foreign pluginId!"); + } + } + + @Override + public boolean enablePlugin(String pluginId) { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute enablePlugin!"); + } + + @Override + public boolean deletePlugin(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.deletePlugin(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute deletePlugin for foreign pluginId!"); + } + } + + @Override + public ClassLoader getPluginClassLoader(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getPluginClassLoader(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getPluginClassLoader for foreign pluginId!"); + } + } + + @Override + public List> getExtensionClasses(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getExtensionClasses(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getExtensionClasses for foreign pluginId!"); + } + } + + @Override + public List> getExtensionClasses(Class type) { + return getExtensionClasses(type, currentPluginId); + } + + @Override + public List> getExtensionClasses(Class type, String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getExtensionClasses(type, pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getExtensionClasses for foreign pluginId!"); + } + } + + @Override + public List getExtensions(Class type) { + return getExtensions(type, currentPluginId); + } + + @Override + public List getExtensions(Class type, String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getExtensions(type, pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getExtensions for foreign pluginId!"); + } + } + + @Override + public List getExtensions(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getExtensions(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getExtensions for foreign pluginId!"); + } + } + + @Override + public Set getExtensionClassNames(String pluginId) { + if (currentPluginId.equals(pluginId)) { + return original.getExtensionClassNames(pluginId); + } else { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getExtensionClassNames for foreign pluginId!"); + } + } + + @Override + public ExtensionFactory getExtensionFactory() { + return original.getExtensionFactory(); + } + + @Override + public RuntimeMode getRuntimeMode() { + return original.getRuntimeMode(); + } + + @Override + public PluginWrapper whichPlugin(Class clazz) { + ClassLoader classLoader = clazz.getClassLoader(); + PluginWrapper plugin = getPlugin(currentPluginId); + if (plugin.getPluginClassLoader() == classLoader) { + return plugin; + } + return null; + } + + @Override + public void addPluginStateListener(PluginStateListener listener) { + if (pluginStateListeners.isEmpty()) { + this.original.addPluginStateListener(listenerWrapper); + } + pluginStateListeners.add(listener); + } + + @Override + public void removePluginStateListener(PluginStateListener listener) { + pluginStateListeners.remove(listener); + if (pluginStateListeners.isEmpty()) { + this.original.removePluginStateListener(listenerWrapper); + } + } + + @Override + public void setSystemVersion(String version) { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute setSystemVersion!"); + } + + @Override + public String getSystemVersion() { + return original.getSystemVersion(); + } + + @Override + public Path getPluginsRoot() { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getPluginsRoot!"); + } + + @Override + public List getPluginsRoots() { + throw new IllegalAccessError(PLUGIN_PREFIX + currentPluginId + " tried to execute getPluginsRoots!"); + } + + @Override + public VersionManager getVersionManager() { + return original.getVersionManager(); + } + + /** + * Wrapper for PluginStateListener events. will only propagate events if they match the current pluginId + * @author Wolfram Haussig + * + */ + private class PluginStateListenerWrapper implements PluginStateListener { + + @Override + public void pluginStateChanged(PluginStateEvent event) { + if (event.getPlugin().getPluginId().equals(currentPluginId)) { + for (PluginStateListener listener : pluginStateListeners) { + listener.pluginStateChanged(event); + } + } + } + + } +} diff --git a/pf4j/src/test/java/org/pf4j/SecurePluginManagerWrapperTest.java b/pf4j/src/test/java/org/pf4j/SecurePluginManagerWrapperTest.java new file mode 100644 index 0000000..f42d83e --- /dev/null +++ b/pf4j/src/test/java/org/pf4j/SecurePluginManagerWrapperTest.java @@ -0,0 +1,272 @@ +package org.pf4j; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.pf4j.test.PluginJar; +import org.pf4j.test.TestExtension; +import org.pf4j.test.TestExtensionPoint; +import org.pf4j.test.TestPlugin; + +public class SecurePluginManagerWrapperTest { + + private static final String OTHER_PLUGIN_ID = "test-plugin-2"; + private static final String THIS_PLUGIN_ID = "test-plugin-1"; + private PluginJar thisPlugin; + private PluginJar otherPlugin; + private PluginManager pluginManager; + private PluginManager wrappedPluginManager; + private int pluginManagerEvents = 0; + private int wrappedPluginManagerEvents = 0; + + @TempDir + Path pluginsPath; + + @BeforeEach + public void setUp() throws IOException { + pluginManagerEvents = 0; + wrappedPluginManagerEvents = 0; + thisPlugin = new PluginJar.Builder(pluginsPath.resolve("test-plugin1.jar"), THIS_PLUGIN_ID).pluginClass(TestPlugin.class.getName()).pluginVersion("1.2.3").extension(TestExtension.class.getName()).build(); + otherPlugin = new PluginJar.Builder(pluginsPath.resolve("test-plugin2.jar"), OTHER_PLUGIN_ID).pluginClass(TestPlugin.class.getName()).pluginVersion("1.2.3").extension(TestExtension.class.getName()).build(); + + pluginManager = new JarPluginManager(pluginsPath); + wrappedPluginManager = new SecurePluginManagerWrapper(pluginManager, THIS_PLUGIN_ID); + } + + @AfterEach + public void tearDown() { + pluginManager.unloadPlugins(); + + thisPlugin = null; + otherPlugin = null; + pluginManager = null; + } + + @Test + public void pluginStateListeners() { + pluginManager.addPluginStateListener(new PluginStateListener() { + @Override + public void pluginStateChanged(PluginStateEvent event) { + pluginManagerEvents++; + } + }); + wrappedPluginManager.addPluginStateListener(new PluginStateListener() { + @Override + public void pluginStateChanged(PluginStateEvent event) { + wrappedPluginManagerEvents++; + } + }); + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + assertEquals(4, pluginManagerEvents); + assertEquals(2, wrappedPluginManagerEvents); + } + + @Test + public void deletePlugin() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.deletePlugin(OTHER_PLUGIN_ID)); + assertTrue(wrappedPluginManager.deletePlugin(THIS_PLUGIN_ID)); + } + + @Test + public void disablePlugin() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.disablePlugin(OTHER_PLUGIN_ID)); + assertTrue(wrappedPluginManager.disablePlugin(THIS_PLUGIN_ID)); + } + + @Test + public void enablePlugin() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.enablePlugin(OTHER_PLUGIN_ID)); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.enablePlugin(THIS_PLUGIN_ID)); + } + + @Test + public void getExtensionClasses() { + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + assertEquals(1, wrappedPluginManager.getExtensionClasses(TestExtensionPoint.class).size()); + + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getExtensionClasses(TestExtensionPoint.class, OTHER_PLUGIN_ID)); + assertEquals(1, wrappedPluginManager.getExtensionClasses(TestExtensionPoint.class, THIS_PLUGIN_ID).size()); + + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getExtensionClasses(OTHER_PLUGIN_ID)); + assertEquals(1, wrappedPluginManager.getExtensionClasses(THIS_PLUGIN_ID).size()); + } + + @Test + public void getExtensionClassNames() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getExtensionClassNames(OTHER_PLUGIN_ID)); + assertEquals(1, wrappedPluginManager.getExtensionClassNames(THIS_PLUGIN_ID).size()); + } + + @Test + public void getExtensionFactory() { + pluginManager.loadPlugins(); + assertEquals(pluginManager.getExtensionFactory(), wrappedPluginManager.getExtensionFactory()); + } + + @Test + public void getExtensions() { + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + assertEquals(1, wrappedPluginManager.getExtensions(TestExtensionPoint.class).size()); + + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getExtensions(TestExtensionPoint.class, OTHER_PLUGIN_ID)); + assertEquals(1, wrappedPluginManager.getExtensions(TestExtensionPoint.class, THIS_PLUGIN_ID).size()); + + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getExtensions(OTHER_PLUGIN_ID)); + assertEquals(1, wrappedPluginManager.getExtensions(THIS_PLUGIN_ID).size()); + } + + @Test + public void getPlugin() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getPlugin(OTHER_PLUGIN_ID)); + assertEquals(THIS_PLUGIN_ID, wrappedPluginManager.getPlugin(THIS_PLUGIN_ID).getPluginId()); + } + + @Test + public void getPluginClassLoader() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getPluginClassLoader(OTHER_PLUGIN_ID)); + assertNotNull(wrappedPluginManager.getPluginClassLoader(THIS_PLUGIN_ID)); + } + + @Test + public void getPlugins() { + pluginManager.loadPlugins(); + assertEquals(2, pluginManager.getPlugins().size()); + assertEquals(1, wrappedPluginManager.getPlugins().size()); + } + + @Test + public void getPluginsRoot() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getPluginsRoot()); + } + + @Test + public void getPluginsRoots() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.getPluginsRoots()); + } + + @Test + public void getResolvedPlugins() { + pluginManager.loadPlugins(); + assertEquals(2, pluginManager.getResolvedPlugins().size()); + assertEquals(1, wrappedPluginManager.getResolvedPlugins().size()); + } + + @Test + public void getRuntimeMode() { + assertEquals(pluginManager.getRuntimeMode(), wrappedPluginManager.getRuntimeMode()); + } + + @Test + public void getStartedPlugins() { + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + assertEquals(2, pluginManager.getStartedPlugins().size()); + assertEquals(1, wrappedPluginManager.getStartedPlugins().size()); + } + + @Test + public void getSystemVersion() { + assertEquals(pluginManager.getSystemVersion(), wrappedPluginManager.getSystemVersion()); + } + + @Test + public void getUnresolvedPlugins() { + assertNotNull(wrappedPluginManager); + assertNotNull(wrappedPluginManager.getUnresolvedPlugins()); + assertTrue(wrappedPluginManager.getUnresolvedPlugins().isEmpty()); + } + + @Test + public void getVersionManager() { + assertEquals(pluginManager.getVersionManager(), wrappedPluginManager.getVersionManager()); + } + + @Test + public void isDevelopment() { + assertEquals(pluginManager.isDevelopment(), wrappedPluginManager.isDevelopment()); + } + + @Test + public void isNotDevelopment() { + assertEquals(pluginManager.isNotDevelopment(), wrappedPluginManager.isNotDevelopment()); + } + + @Test + public void loadPlugin() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.loadPlugin(thisPlugin.path())); + } + + @Test + public void loadPlugins() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.loadPlugins()); + } + + @Test + public void setSystemVersion() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.setSystemVersion("1.0.0")); + } + + @Test + public void startPlugin() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.startPlugin(OTHER_PLUGIN_ID)); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.startPlugin(THIS_PLUGIN_ID)); + } + + @Test + public void startPlugins() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.startPlugins()); + } + + @Test + public void stopPlugin() { + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.stopPlugin(OTHER_PLUGIN_ID)); + assertEquals(PluginState.STOPPED, wrappedPluginManager.stopPlugin(THIS_PLUGIN_ID)); + } + + @Test + public void stopPlugins() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.stopPlugins()); + } + + @Test + public void unloadPlugin() { + pluginManager.loadPlugins(); + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.unloadPlugin(OTHER_PLUGIN_ID)); + assertTrue(wrappedPluginManager.unloadPlugin(THIS_PLUGIN_ID)); + } + + @Test + public void unloadPlugins() { + assertThrows(IllegalAccessError.class, () -> wrappedPluginManager.unloadPlugins()); + } + + @Test + public void whichPlugin() { + pluginManager.loadPlugins(); + pluginManager.startPlugins(); + assertEquals(null, wrappedPluginManager.whichPlugin(pluginManager.getExtensionClasses(OTHER_PLUGIN_ID).get(0))); + assertEquals(THIS_PLUGIN_ID, wrappedPluginManager.whichPlugin(pluginManager.getExtensionClasses(THIS_PLUGIN_ID).get(0)).getPluginId()); + } +}