Rusty machine

Java makes it possible to compile Java code at runtime… any Java code.

The entry-point to the compilation is the ToolProvider class. From its Javadoc:

Provides methods for locating tool providers, for example, providers of compilers. This class complements the functionality of ServiceLoader.

This class is available in Java since version 1.6 - released 10 years ago, but seems to have been largely ignored.

The code

Here’s a snippet that allows that:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class EvilExecutor {

    private String readCode(String sourcePath) throws FileNotFoundException {
        InputStream stream = new FileInputStream(sourcePath);
        String separator = System.getProperty("line.separator");
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        return reader.lines().collect(Collectors.joining(separator));
    }

    private Path saveSource(String source) throws IOException {
        String tmpProperty = System.getProperty("java.io.tmpdir");
        Path sourcePath = Paths.get(tmpProperty, "Harmless.java");
        Files.write(sourcePath, source.getBytes(UTF_8));
        return sourcePath;
    }

    private Path compileSource(Path javaFile) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        compiler.run(null, null, null, javaFile.toFile().getAbsolutePath());
        return javaFile.getParent().resolve("Harmless.class");
    }

    private void runClass(Path javaClass)
            throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
        URL classUrl = javaClass.getParent().toFile().toURI().toURL();
        URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{classUrl});
        Class<?> clazz = Class.forName("Harmless", true, classLoader);
        clazz.newInstance();
    }

    public void doEvil(String sourcePath) throws Exception {
        String source = readCode(sourcePath);
        Path javaFile = saveSource(source);
        Path classFile = compileSource(javaFile);
        runClass(classFile);
    }

    public static void main(String... args) throws Exception {
        new EvilExecutor().doEvil(args[0]);
    }
}

Some explanations are in order:

  • readCode(): reads the source code from an arbitrary file on the file system, and returns it as a string. An alternative implementation would get the source from across the network.
  • saveSource(): creates a new file from the source code in a read-enabled directory. The file name is hard-coded, more refined versions would parse the code parameter to create a file named according to the class name it contains.
  • compileSource(): compiles the class file out of the java file.
  • runClass: loads the compiled class and instantiates a new object. To be independent from any cast, the to-be-executed code should be set in the constructor of the external source code class.

The issue

From a feature point of view, compiling code on the fly boosts the value of the Java language compared to others that don’t provide this feature. From a security point of view, this is a nightmare. The thought of being able to execute arbitrary code in production should send shivers down anyone’s spine who is part of any IT organization, developers included, if not mostly.

Seasoned developers/ops or regular readers probably remember about the Java security manager and how to activate it:

java -Djava.security.manager -cp target/classes ch.frankel.blog.runtimecompile.EvilExecutor harmless.txt

Executing the above command-line will yield the following result:

java.lang.SecurityManager@4e25154f
Exception in thread "main" java.security.AccessControlException:
    access denied ("java.io.FilePermission" "harmless.txt" "read")
  at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
  at java.security.AccessController.checkPermission(AccessController.java:884)
  at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
  at java.lang.SecurityManager.checkRead(SecurityManager.java:888)
  at java.io.FileInputStream.<init>(FileInputStream.java:127)
  at java.io.FileInputStream.<init>(FileInputStream.java:93)
  at ch.frankel.blog.runtimecompile.EvilExecutor.readCode(EvilExecutor.java:19)
  at ch.frankel.blog.runtimecompile.EvilExecutor.doEvil(EvilExecutor.java:47)
  at ch.frankel.blog.runtimecompile.EvilExecutor.main(EvilExecutor.java:56)

Conclusion

The JVM offers plenty of features. A with any tool, they can be used for good or bad. It’s up to everyone to feel responsible about properly securing one’s JVM, doubly so in sensitive fields - banking, military, etc.