Skip to content

Commit 4f9916d

Browse files
authored
Block file access outside instance dir (#141)
* sandbox security handler * fix errors * remove global minecraft home var * null check parent security manager * use security manager on closures
1 parent 8c16c6a commit 4f9916d

File tree

6 files changed

+136
-11
lines changed

6 files changed

+136
-11
lines changed

src/main/java/com/cleanroommc/groovyscript/GroovyScript.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class GroovyScript {
8484

8585
public static final Logger LOGGER = LogManager.getLogger(ID);
8686

87+
private static File minecraftHome;
8788
private static File scriptPath;
8889
private static File runConfigFile;
8990
private static File resourcesFile;
@@ -139,6 +140,7 @@ public void onRegisterItem(RegistryEvent.Register<Item> event) {
139140

140141
@ApiStatus.Internal
141142
public static void initializeRunConfig(File minecraftHome) {
143+
GroovyScript.minecraftHome = minecraftHome;
142144
// If we are launching with the environment variable set to use the examples folder, use the examples folder for easy and consistent testing.
143145
if (Boolean.parseBoolean(System.getProperty("groovyscript.use_examples_folder"))) {
144146
scriptPath = new File(minecraftHome.getParentFile(), "examples");
@@ -207,6 +209,14 @@ public static String getScriptPath() {
207209
return getScriptFile().getPath();
208210
}
209211

212+
@NotNull
213+
public static File getMinecraftHome() {
214+
if (minecraftHome == null) {
215+
throw new IllegalStateException("GroovyScript is not yet loaded!");
216+
}
217+
return minecraftHome;
218+
}
219+
210220
@NotNull
211221
public static File getScriptFile() {
212222
if (scriptPath == null) {

src/main/java/com/cleanroommc/groovyscript/helper/GroovyHelper.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.cleanroommc.groovyscript.packmode.Packmode;
55
import com.cleanroommc.groovyscript.api.GroovyBlacklist;
66
import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager;
7+
import com.cleanroommc.groovyscript.sandbox.FileUtil;
78
import com.cleanroommc.groovyscript.sandbox.LoadStage;
89
import net.minecraftforge.fml.common.FMLCommonHandler;
910
import net.minecraftforge.fml.common.Loader;
@@ -83,4 +84,12 @@ public static String getPackmode() {
8384
public static boolean isPackmode(String packmode) {
8485
return getPackmode().equalsIgnoreCase(packmode);
8586
}
87+
88+
public static String getMinecraftHome() {
89+
return GroovyScript.getMinecraftHome().getPath();
90+
}
91+
92+
public static File file(String... parts) {
93+
return new File(GroovyScript.getMinecraftHome(), FileUtil.makePath(parts));
94+
}
8695
}

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovySandbox.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,7 @@ protected void loadScripts(GroovyScriptEngine engine, Binding binding, Set<File>
148148
}
149149
if (shouldRunFile(scriptFile)) {
150150
Script script = InvokerHelper.createScript(clazz, binding);
151-
if (run) {
152-
setCurrentScript(scriptFile.toString());
153-
script.run();
154-
setCurrentScript(null);
155-
}
151+
if (run) runScript(script);
156152
}
157153
}
158154
}
@@ -172,15 +168,17 @@ protected void loadClassScripts(GroovyScriptEngine engine, Binding binding, Set<
172168
if (clazz.getSuperclass() != Script.class && shouldRunFile(classFile)) {
173169
executedClasses.add(classFile);
174170
Script script = InvokerHelper.createScript(clazz, binding);
175-
if (run) {
176-
setCurrentScript(script.toString());
177-
script.run();
178-
setCurrentScript(null);
179-
}
171+
if (run) runScript(script);
180172
}
181173
}
182174
}
183175

176+
protected void runScript(Script script){
177+
setCurrentScript(script.toString());
178+
script.run();
179+
setCurrentScript(null);
180+
}
181+
184182
public <T> T runClosure(Closure<T> closure, Object... args) {
185183
startRunning();
186184
T result = null;

src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptSandbox.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import com.cleanroommc.groovyscript.helper.GroovyHelper;
1010
import com.cleanroommc.groovyscript.helper.JsonHelper;
1111
import com.cleanroommc.groovyscript.registry.ReloadableRegistryManager;
12+
import com.cleanroommc.groovyscript.sandbox.security.SandboxSecurityManager;
1213
import com.cleanroommc.groovyscript.sandbox.transformer.GroovyScriptCompiler;
1314
import com.google.gson.JsonArray;
1415
import com.google.gson.JsonElement;
1516
import com.google.gson.JsonObject;
1617
import groovy.lang.Binding;
1718
import groovy.lang.Closure;
1819
import groovy.lang.GroovyClassLoader;
20+
import groovy.lang.Script;
1921
import groovy.util.GroovyScriptEngine;
2022
import groovy.util.ResourceException;
2123
import groovy.util.ScriptException;
@@ -39,6 +41,8 @@
3941

4042
public class GroovyScriptSandbox extends GroovySandbox {
4143

44+
private static final SandboxSecurityManager securityManager = new SandboxSecurityManager();
45+
4246
private final File cacheRoot;
4347
private final File scriptRoot;
4448
private final ImportCustomizer importCustomizer = new ImportCustomizer();
@@ -64,7 +68,6 @@ public GroovyScriptSandbox(File scriptRoot, File cacheRoot) throws MalformedURLE
6468

6569
this.importCustomizer.addStaticStars(GroovyHelper.class.getName(), MathHelper.class.getName());
6670
registerStaticImports(GroovyHelper.class, MathHelper.class);
67-
6871
this.importCustomizer.addImports("net.minecraft.world.World",
6972
"net.minecraft.block.state.IBlockState",
7073
"net.minecraft.block.Block",
@@ -162,6 +165,13 @@ public void run(LoadStage currentLoadStage) {
162165
}
163166
}
164167

168+
@Override
169+
protected void runScript(Script script) {
170+
securityManager.install();
171+
super.runScript(script);
172+
securityManager.uninstall();
173+
}
174+
165175
@ApiStatus.Internal
166176
@Override
167177
public void load() throws Exception {
@@ -171,6 +181,7 @@ public void load() throws Exception {
171181
@Override
172182
public <T> T runClosure(Closure<T> closure, Object... args) {
173183
startRunning();
184+
securityManager.install();
174185
T result = null;
175186
try {
176187
result = closure.call(args);
@@ -181,6 +192,7 @@ public <T> T runClosure(Closure<T> closure, Object... args) {
181192
return new AtomicInteger();
182193
}).addAndGet(1);
183194
} finally {
195+
securityManager.uninstall();
184196
stopRunning();
185197
}
186198
return result;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.cleanroommc.groovyscript.sandbox.security;
2+
3+
import com.cleanroommc.groovyscript.GroovyScript;
4+
import net.minecraftforge.fml.relauncher.FMLLaunchHandler;
5+
import sun.misc.Unsafe;
6+
7+
import java.io.FilePermission;
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
import java.security.Permission;
11+
12+
public class SandboxSecurityManager extends SecurityManager {
13+
14+
private static final Object securityFieldBase;
15+
private static final long securityFieldOffset;
16+
private static final Unsafe UNSAFE;
17+
private final SecurityManager parent;
18+
19+
// cursed
20+
static {
21+
Object base = null;
22+
long offset = 0;
23+
Unsafe unsafe;
24+
try {
25+
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
26+
unsafeField.setAccessible(true);
27+
unsafe = (Unsafe) unsafeField.get(null);
28+
29+
Method getFields = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
30+
getFields.setAccessible(true);
31+
for (Field field : (Field[]) getFields.invoke(System.class, false)) {
32+
if (field.getName().equals("security")) {
33+
offset = unsafe.staticFieldOffset(field);
34+
base = unsafe.staticFieldBase(field);
35+
break;
36+
}
37+
}
38+
} catch (Throwable e) {
39+
throw new RuntimeException(e);
40+
}
41+
securityFieldBase = base;
42+
securityFieldOffset = offset;
43+
UNSAFE = unsafe;
44+
}
45+
46+
public SandboxSecurityManager() {
47+
this.parent = System.getSecurityManager();
48+
}
49+
50+
public void install() {
51+
UNSAFE.putObject(securityFieldBase, securityFieldOffset, this);
52+
}
53+
54+
public void uninstall() {
55+
UNSAFE.putObject(securityFieldBase, securityFieldOffset, this.parent);
56+
}
57+
58+
public void checkFile(Permission perm) {
59+
if (perm instanceof FilePermission filePerm) {
60+
String path = filePerm.getName();
61+
Class<?>[] classContext = getClassContext();
62+
if (!path.startsWith(GroovyScript.getMinecraftHome().getPath())) {
63+
if (FMLLaunchHandler.isDeobfuscatedEnvironment() && path.startsWith(GroovyScript.getScriptPath())) return;
64+
for (Class<?> clazz : classContext) {
65+
if (ClassLoader.class.isAssignableFrom(clazz)) {
66+
// allow loading classes
67+
return;
68+
}
69+
}
70+
throw new SecurityException("Only files in minecraft home and sub directories can be accessed from scripts! Tried to access " + perm.getName());
71+
}
72+
}
73+
}
74+
75+
@Override
76+
public Object getSecurityContext() {
77+
return parent != null ? parent.getSecurityContext() : super.getSecurityContext();
78+
}
79+
80+
@Override
81+
public void checkPermission(Permission perm) {
82+
if (parent != null) parent.checkPermission(perm);
83+
checkFile(perm);
84+
}
85+
86+
@Override
87+
public void checkPermission(Permission perm, Object context) {
88+
if (parent != null) parent.checkPermission(perm, context);
89+
checkFile(perm);
90+
}
91+
}

src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/GroovyScriptTransformer.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.cleanroommc.groovyscript.sandbox.transformer;
22

3+
import com.cleanroommc.groovyscript.api.GroovyLog;
34
import com.cleanroommc.groovyscript.gameobjects.GameObjectHandlerManager;
45
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
56
import org.codehaus.groovy.ast.ClassHelper;
@@ -8,6 +9,7 @@
89
import org.codehaus.groovy.ast.expr.*;
910
import org.codehaus.groovy.control.SourceUnit;
1011

12+
import java.io.File;
1113
import java.util.ArrayList;
1214
import java.util.List;
1315

@@ -49,6 +51,9 @@ private Expression transformInternal(Expression expr) {
4951
if (expr instanceof MethodCallExpression) {
5052
return checkValid((MethodCallExpression) expr);
5153
}
54+
if (expr instanceof ConstructorCallExpression cce && cce.getType().getName().equals(File.class.getName())) {
55+
GroovyLog.get().warn("Detected `new File(...)` usage. Use `file(...)` instead!");
56+
}
5257
return expr;
5358
}
5459

0 commit comments

Comments
 (0)