From b980d46e0b416fac9fabc8649a609b48330a8b06 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Fri, 15 Mar 2024 19:47:40 +0100 Subject: [PATCH] sandbox security handler --- .../groovyscript/GroovyScript.java | 10 ++ .../groovyscript/helper/GroovyHelper.java | 9 ++ .../groovyscript/sandbox/GroovySandbox.java | 18 ++-- .../sandbox/GroovyScriptSandbox.java | 13 ++- .../security/SandboxSecurityManager.java | 92 +++++++++++++++++++ .../transformer/GroovyScriptTransformer.java | 5 + 6 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/cleanroommc/groovyscript/sandbox/security/SandboxSecurityManager.java diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index aad384d3b..7e2b1e027 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -84,6 +84,7 @@ public class GroovyScript { public static final Logger LOGGER = LogManager.getLogger(ID); + private static File minecraftHome; private static File scriptPath; private static File runConfigFile; private static File resourcesFile; @@ -140,6 +141,7 @@ public void onRegisterItem(RegistryEvent.Register event) { @ApiStatus.Internal public static void initializeRunConfig(File minecraftHome) { + GroovyScript.minecraftHome = minecraftHome; // If we are launching with the environment variable set to use the examples folder, use the examples folder for easy and consistent testing. if (Boolean.parseBoolean(System.getProperty("groovyscript.use_examples_folder"))) { scriptPath = new File(minecraftHome.getParentFile(), "examples"); @@ -208,6 +210,14 @@ public static String getScriptPath() { return getScriptFile().getPath(); } + @NotNull + public static File getMinecraftHome() { + if (minecraftHome == null) { + throw new IllegalStateException("GroovyScript is not yet loaded!"); + } + return minecraftHome; + } + @NotNull public static File getScriptFile() { if (scriptPath == null) { diff --git a/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java b/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java index 88b43b6d2..c6251f017 100644 --- a/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java +++ b/src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java @@ -4,6 +4,7 @@ import com.cleanroommc.groovyscript.packmode.Packmode; import com.cleanroommc.groovyscript.api.GroovyBlacklist; import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; +import com.cleanroommc.groovyscript.sandbox.FileUtil; import com.cleanroommc.groovyscript.sandbox.LoadStage; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.Loader; @@ -83,4 +84,12 @@ public static String getPackmode() { public static boolean isPackmode(String packmode) { return getPackmode().equalsIgnoreCase(packmode); } + + public static String getMinecraftHome() { + return GroovyScript.getMinecraftHome().getPath(); + } + + public static File file(String... parts) { + return new File(GroovyScript.getMinecraftHome(), FileUtil.makePath(parts)); + } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java index b1f1457ce..2ff94f2ce 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java @@ -148,11 +148,7 @@ protected void loadScripts(GroovyScriptEngine engine, Binding binding, Set } if (shouldRunFile(scriptFile)) { Script script = InvokerHelper.createScript(clazz, binding); - if (run) { - setCurrentScript(scriptFile.toString()); - script.run(); - setCurrentScript(null); - } + if (run) runScript(script); } } } @@ -172,15 +168,17 @@ protected void loadClassScripts(GroovyScriptEngine engine, Binding binding, Set< if (clazz.getSuperclass() != Script.class && shouldRunFile(classFile)) { executedClasses.add(classFile); Script script = InvokerHelper.createScript(clazz, binding); - if (run) { - setCurrentScript(script.toString()); - script.run(); - setCurrentScript(null); - } + if (run) runScript(script); } } } + protected void runScript(Script script){ + setCurrentScript(script.toString()); + script.run(); + setCurrentScript(null); + } + public T runClosure(Closure closure, Object... args) { startRunning(); T result = null; diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java index eca29fd7e..7bb90b131 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java @@ -9,6 +9,7 @@ import com.cleanroommc.groovyscript.helper.GroovyHelper; import com.cleanroommc.groovyscript.helper.JsonHelper; import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager; +import com.cleanroommc.groovyscript.sandbox.security.SandboxSecurityManager; import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -16,6 +17,7 @@ import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.GroovyClassLoader; +import groovy.lang.Script; import groovy.util.GroovyScriptEngine; import groovy.util.ResourceException; import groovy.util.ScriptException; @@ -39,6 +41,8 @@ public class GroovyScriptSandbox extends GroovySandbox { + private static final SandboxSecurityManager securityManager = new SandboxSecurityManager(); + private final File cacheRoot; private final File scriptRoot; private final ImportCustomizer importCustomizer = new ImportCustomizer(); @@ -61,10 +65,10 @@ public GroovyScriptSandbox(File scriptRoot, File cacheRoot) throws MalformedURLE registerBinding("Mods", ModSupport.INSTANCE); registerBinding("Log", GroovyLog.get()); registerBinding("EventManager", GroovyEventManager.INSTANCE); + registerBinding("MinecraftHome", GroovyScript.getMinecraftHome()); this.importCustomizer.addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName()); registerStaticImports(GroovyHelper.class, MathHelper.class); - this.importCustomizer.addImports("net.minecraft.world.World", "net.minecraft.block.state.IBlockState", "net.minecraft.block.Block", @@ -162,6 +166,13 @@ public void run(LoadStage currentLoadStage) { } } + @Override + protected void runScript(Script script) { + securityManager.install(); + super.runScript(script); + securityManager.uninstall(); + } + @ApiStatus.Internal @Override public void load() throws Exception { diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/security/SandboxSecurityManager.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/SandboxSecurityManager.java new file mode 100644 index 000000000..fffeadf95 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/security/SandboxSecurityManager.java @@ -0,0 +1,92 @@ +package com.cleanroommc.groovyscript.sandbox.security; + +import com.cleanroommc.groovyscript.GroovyScript; +import sun.misc.Unsafe; + +import java.io.FilePermission; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.security.Permission; + +public class SandboxSecurityManager extends SecurityManager { + + private static final Object securityFieldBase; + private static final long securityFieldOffset; + private static final Unsafe UNSAFE; + private final SecurityManager parent; + + // cursed + static { + Object base = null; + long offset = 0; + Unsafe unsafe; + try { + Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + unsafe = (Unsafe) unsafeField.get(null); + + Method getFields = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class); + getFields.setAccessible(true); + for (Field field : (Field[]) getFields.invoke(System.class, false)) { + if (field.getName().equals("security")) { + offset = unsafe.staticFieldOffset(field); + base = unsafe.staticFieldBase(field); + break; + } + } + } catch (Throwable e) { + throw new RuntimeException(e); + } + securityFieldBase = base; + securityFieldOffset = offset; + UNSAFE = unsafe; + } + + public SandboxSecurityManager() { + this.parent = System.getSecurityManager(); + if (this.parent == null) { + throw new NullPointerException(); + } + } + + public void install() { + UNSAFE.putObject(securityFieldBase, securityFieldOffset, this); + } + + public void uninstall() { + System.setSecurityManager(this.parent); + } + + public void checkFile(Permission perm) { + if (perm instanceof FilePermission filePerm) { + String path = filePerm.getName(); + Class[] classContext = getClassContext(); + if (!path.startsWith(GroovyScript.getMinecraftHome().getPath())) { + for (Class clazz : classContext) { + if (ClassLoader.class.isAssignableFrom(clazz)) { + // allow loading classes + return; + } + } + throw new SecurityException("Only files in minecraft home and sub directories can be accessed from scripts! Tried to access " + perm.getName()); + } + } + } + + @Override + public Object getSecurityContext() { + return parent.getSecurityContext(); + } + + @Override + public void checkPermission(Permission perm) { + parent.checkPermission(perm); + checkFile(perm); + } + + @Override + public void checkPermission(Permission perm, Object context) { + parent.checkPermission(perm, context); + checkFile(perm); + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptTransformer.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptTransformer.java index cb82be2bd..2174f7d2e 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptTransformer.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptTransformer.java @@ -1,5 +1,6 @@ package com.cleanroommc.groovyscript.sandbox.transformer; +import com.cleanroommc.groovyscript.api.GroovyLog; import com.cleanroommc.groovyscript.gameobjects.GameObjectHandlerManager; import org.codehaus.groovy.ast.ClassCodeExpressionTransformer; import org.codehaus.groovy.ast.ClassHelper; @@ -8,6 +9,7 @@ import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.control.SourceUnit; +import java.io.File; import java.util.ArrayList; import java.util.List; @@ -49,6 +51,9 @@ private Expression transformInternal(Expression expr) { if (expr instanceof MethodCallExpression) { return checkValid((MethodCallExpression) expr); } + if (expr instanceof ConstructorCallExpression cce && cce.getType().getName().equals(File.class.getName())) { + GroovyLog.get().warn("Detected `new File(...)` usage. Use `file(...)` instead!"); + } return expr; }