diff --git a/build.gradle b/build.gradle index b2b8958..31c7041 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,8 @@ dependencies { // todo once we fix the logging properties set this to compile testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.25' + implementation 'com.github.I-Al-Istannen:JvmAgentUtils:de8d42398b' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: junitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junitVersion } diff --git a/src/main/java/org/togetherjava/discord/server/EventHandler.java b/src/main/java/org/togetherjava/discord/server/EventHandler.java index 6cefbaf..e9ec168 100644 --- a/src/main/java/org/togetherjava/discord/server/EventHandler.java +++ b/src/main/java/org/togetherjava/discord/server/EventHandler.java @@ -5,6 +5,7 @@ import jdk.jshell.SnippetEvent; import org.togetherjava.discord.server.execution.JShellSessionManager; import org.togetherjava.discord.server.execution.JShellWrapper; +import org.togetherjava.discord.server.io.input.InputSanitizerManager; import org.togetherjava.discord.server.rendering.RendererManager; import sx.blah.discord.api.events.EventSubscriber; import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent; @@ -22,12 +23,14 @@ public class EventHandler { private JShellSessionManager jShellSessionManager; private final String botPrefix; private RendererManager rendererManager; + private InputSanitizerManager sanitizerManager; @SuppressWarnings("WeakerAccess") public EventHandler(Config config) { this.jShellSessionManager = new JShellSessionManager(config); this.botPrefix = config.getString("prefix"); this.rendererManager = new RendererManager(); + this.sanitizerManager = new InputSanitizerManager(); } @EventSubscriber @@ -51,7 +54,7 @@ private String parseCommandFromMessage(String messageContent) { return codeBlockMatcher.group(2); } - return withoutPrefix; + return sanitizerManager.sanitize(withoutPrefix); } private void executeCommand(IUser user, JShellWrapper shell, String command, IChannel channel) { diff --git a/src/main/java/org/togetherjava/discord/server/JShellBot.java b/src/main/java/org/togetherjava/discord/server/JShellBot.java index 7dc56c0..58c4189 100644 --- a/src/main/java/org/togetherjava/discord/server/JShellBot.java +++ b/src/main/java/org/togetherjava/discord/server/JShellBot.java @@ -25,7 +25,6 @@ public static void main(String[] args) { } /** - * * @throws Exception */ public void start() throws Exception { @@ -37,21 +36,19 @@ public void start() throws Exception { Path botConfigPath = botConfigPathString == null ? null : Paths.get(botConfigPathString); - if(botConfigPath == null){ + if (botConfigPath == null) { Properties prop = new Properties(); prop.load(JShellBot.class.getResourceAsStream("/bot.properties")); config = new Config(prop); - } - else{ + } else { config = new Config(botConfigPath); } - if(config.getString("token") != null){ + if (config.getString("token") != null) { IDiscordClient client = BotUtils.buildDiscordClient(config.getString("token")); client.getDispatcher().registerListener(new EventHandler(config)); client.login(); - } - else{ + } else { log.error("Token not set or config file not found in"); exit(1); } diff --git a/src/main/java/org/togetherjava/discord/server/JshellSecurityManager.java b/src/main/java/org/togetherjava/discord/server/JshellSecurityManager.java new file mode 100644 index 0000000..f7c9189 --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/JshellSecurityManager.java @@ -0,0 +1,26 @@ +package org.togetherjava.discord.server; + +import java.security.Permission; +import java.util.Arrays; + +public class JshellSecurityManager extends SecurityManager { + + @Override + public void checkPermission(Permission perm) { + // allow all but Jshell to bypass this + if (!comesFromMe() && comesFromJshell()) { + super.checkPermission(perm); + } + } + + private boolean comesFromJshell() { + return Arrays.stream(getClassContext()) + .anyMatch(aClass -> aClass.getName().contains("REPL")); + } + + private boolean comesFromMe() { + return Arrays.stream(getClassContext()) + .skip(2) + .anyMatch(aClass -> aClass == getClassContext()[0]); + } +} diff --git a/src/main/java/org/togetherjava/discord/server/execution/JShellWrapper.java b/src/main/java/org/togetherjava/discord/server/execution/JShellWrapper.java index 699765a..e41a4d8 100644 --- a/src/main/java/org/togetherjava/discord/server/execution/JShellWrapper.java +++ b/src/main/java/org/togetherjava/discord/server/execution/JShellWrapper.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.Logger; import org.togetherjava.discord.server.Config; import org.togetherjava.discord.server.io.StringOutputStream; +import org.togetherjava.discord.server.sandbox.AgentAttacher; import org.togetherjava.discord.server.sandbox.FilteredExecutionControlProvider; import org.togetherjava.discord.server.sandbox.Sandbox; @@ -43,6 +44,11 @@ private JShell buildJShell(OutputStream outputStream, Config config) { return JShell.builder() .out(out) .err(out) + .remoteVMOptions( + AgentAttacher.getCommandLineArgument(), + "-Djava.security.policy==" + + getClass().getResource("/jshell.policy").toExternalForm() + ) .executionEngine(getExecutionControlProvider(config), Map.of()) .build(); } catch (UnsupportedEncodingException e) { diff --git a/src/main/java/org/togetherjava/discord/server/io/input/InputSanitizer.java b/src/main/java/org/togetherjava/discord/server/io/input/InputSanitizer.java new file mode 100644 index 0000000..643c3ff --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/io/input/InputSanitizer.java @@ -0,0 +1,12 @@ +package org.togetherjava.discord.server.io.input; + +public interface InputSanitizer { + + /** + * Sanizizes the input to Jshell so that errors in it might be accounted for. + * + * @param input the input to sanitize + * @return the resulting input + */ + String sanitize(String input); +} diff --git a/src/main/java/org/togetherjava/discord/server/io/input/InputSanitizerManager.java b/src/main/java/org/togetherjava/discord/server/io/input/InputSanitizerManager.java new file mode 100644 index 0000000..a18fc8b --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/io/input/InputSanitizerManager.java @@ -0,0 +1,42 @@ +package org.togetherjava.discord.server.io.input; + +import java.util.ArrayList; +import java.util.List; + +public class InputSanitizerManager { + + private List sanitizers; + + public InputSanitizerManager() { + this.sanitizers = new ArrayList<>(); + + addDefaults(); + } + + private void addDefaults() { + addSanitizer(new UnicodeQuoteSanitizer()); + } + + /** + * Adds a new {@link InputSanitizer} + * + * @param sanitizer the sanitizer to add + */ + public void addSanitizer(InputSanitizer sanitizer) { + sanitizers.add(sanitizer); + } + + /** + * Sanitizes a given input using all registered {@link InputSanitizer}s. + * + * @param input the input to sanitize + * @return the resulting input + */ + public String sanitize(String input) { + String result = input; + for (InputSanitizer sanitizer : sanitizers) { + result = sanitizer.sanitize(input); + } + return result; + } +} diff --git a/src/main/java/org/togetherjava/discord/server/io/input/UnicodeQuoteSanitizer.java b/src/main/java/org/togetherjava/discord/server/io/input/UnicodeQuoteSanitizer.java new file mode 100644 index 0000000..6bec340 --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/io/input/UnicodeQuoteSanitizer.java @@ -0,0 +1,11 @@ +package org.togetherjava.discord.server.io.input; + +public class UnicodeQuoteSanitizer implements InputSanitizer { + + @Override + public String sanitize(String input) { + return input + .replace("“", "\"") + .replace("”", "\""); + } +} diff --git a/src/main/java/org/togetherjava/discord/server/rendering/CompilationErrorRenderer.java b/src/main/java/org/togetherjava/discord/server/rendering/CompilationErrorRenderer.java index 4287eb7..035532f 100644 --- a/src/main/java/org/togetherjava/discord/server/rendering/CompilationErrorRenderer.java +++ b/src/main/java/org/togetherjava/discord/server/rendering/CompilationErrorRenderer.java @@ -13,8 +13,6 @@ public boolean isApplicable(Object param) { @Override public EmbedBuilder render(Object object, EmbedBuilder builder) { - RenderUtils.applyFailColor(builder); - Diag diag = (Diag) object; return builder .appendField("Is compilation error", String.valueOf(diag.isError()), true) diff --git a/src/main/java/org/togetherjava/discord/server/rendering/ExceptionRenderer.java b/src/main/java/org/togetherjava/discord/server/rendering/ExceptionRenderer.java index 2fae82f..326af70 100644 --- a/src/main/java/org/togetherjava/discord/server/rendering/ExceptionRenderer.java +++ b/src/main/java/org/togetherjava/discord/server/rendering/ExceptionRenderer.java @@ -12,8 +12,6 @@ public boolean isApplicable(Object param) { @Override public EmbedBuilder render(Object object, EmbedBuilder builder) { - RenderUtils.applyFailColor(builder); - Throwable throwable = (Throwable) object; builder .appendField("Exception type", throwable.getClass().getSimpleName(), true) diff --git a/src/main/java/org/togetherjava/discord/server/rendering/RejectedColorRenderer.java b/src/main/java/org/togetherjava/discord/server/rendering/RejectedColorRenderer.java new file mode 100644 index 0000000..2f8755f --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/rendering/RejectedColorRenderer.java @@ -0,0 +1,23 @@ +package org.togetherjava.discord.server.rendering; + +import jdk.jshell.Snippet; +import org.togetherjava.discord.server.execution.JShellWrapper; +import sx.blah.discord.util.EmbedBuilder; + +public class RejectedColorRenderer implements Renderer { + @Override + public boolean isApplicable(Object param) { + return param instanceof JShellWrapper.JShellResult; + } + + @Override + public EmbedBuilder render(Object object, EmbedBuilder builder) { + JShellWrapper.JShellResult result = (JShellWrapper.JShellResult) object; + + if (result.getEvents().stream().anyMatch(e -> e.status() == Snippet.Status.REJECTED)) { + RenderUtils.applyFailColor(builder); + } + + return builder; + } +} diff --git a/src/main/java/org/togetherjava/discord/server/rendering/RendererManager.java b/src/main/java/org/togetherjava/discord/server/rendering/RendererManager.java index 2c08b70..aa44b13 100644 --- a/src/main/java/org/togetherjava/discord/server/rendering/RendererManager.java +++ b/src/main/java/org/togetherjava/discord/server/rendering/RendererManager.java @@ -19,6 +19,7 @@ public RendererManager() { addRenderer(new ExceptionRenderer()); addRenderer(new StandardOutputRenderer()); addRenderer(new CompilationErrorRenderer()); + addRenderer(new RejectedColorRenderer()); } /** diff --git a/src/main/java/org/togetherjava/discord/server/sandbox/AgentAttacher.java b/src/main/java/org/togetherjava/discord/server/sandbox/AgentAttacher.java new file mode 100644 index 0000000..bd125c4 --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/sandbox/AgentAttacher.java @@ -0,0 +1,22 @@ +package org.togetherjava.discord.server.sandbox; + +import me.ialistannen.jvmagentutils.instrumentation.JvmUtils; +import org.togetherjava.discord.server.JshellSecurityManager; + +import java.nio.file.Path; + +public class AgentAttacher { + + private static final Path agentJar = JvmUtils.generateAgentJar( + AgentMain.class, AgentMain.class, JshellSecurityManager.class + ); + + /** + * Returns the command line argument that attaches the agent. + * + * @return the command line argument to start it + */ + public static String getCommandLineArgument() { + return "-javaagent:" + agentJar.toAbsolutePath(); + } +} diff --git a/src/main/java/org/togetherjava/discord/server/sandbox/AgentMain.java b/src/main/java/org/togetherjava/discord/server/sandbox/AgentMain.java new file mode 100644 index 0000000..cbb0f51 --- /dev/null +++ b/src/main/java/org/togetherjava/discord/server/sandbox/AgentMain.java @@ -0,0 +1,16 @@ +package org.togetherjava.discord.server.sandbox; + +import org.togetherjava.discord.server.JshellSecurityManager; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; + +/** + * An agent that sets the security manager JShell uses. + */ +public class AgentMain implements ClassFileTransformer { + + public static void premain(String args, Instrumentation inst) { + System.setSecurityManager(new JshellSecurityManager()); + } +} diff --git a/src/main/java/org/togetherjava/discord/server/sandbox/Sandbox.java b/src/main/java/org/togetherjava/discord/server/sandbox/Sandbox.java index 9f4f856..70c2ee6 100644 --- a/src/main/java/org/togetherjava/discord/server/sandbox/Sandbox.java +++ b/src/main/java/org/togetherjava/discord/server/sandbox/Sandbox.java @@ -1,6 +1,9 @@ package org.togetherjava.discord.server.sandbox; -import java.security.*; +import java.security.AccessControlContext; +import java.security.PermissionCollection; +import java.security.Permissions; +import java.security.ProtectionDomain; import java.util.function.Supplier; public class Sandbox { @@ -27,9 +30,10 @@ public Sandbox() { * @return the result of running it */ public T runInSandBox(Supplier supplier) { - return AccessController.doPrivileged( - (PrivilegedAction) supplier::get, - controlContext - ); + return supplier.get(); +// return AccessController.doPrivileged( +// (PrivilegedAction) supplier::get, +// controlContext +// ); } } diff --git a/src/main/resources/jshell.policy b/src/main/resources/jshell.policy new file mode 100644 index 0000000..3c97bbc --- /dev/null +++ b/src/main/resources/jshell.policy @@ -0,0 +1,5 @@ +// Restrict what Jshell can run +grant { + permission java.util.RuntimePermission "accessDeclaredMembers"; + permission java.util.RuntimePermission "accessClassInPackage"; +};