SandboxTest.java

/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2017 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.commons.compiler.tests;

import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.security.AccessControlException;
import java.security.AllPermission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedExceptionAction;
import java.util.List;

import org.codehaus.commons.compiler.IClassBodyEvaluator;
import org.codehaus.commons.compiler.ICompilerFactory;
import org.codehaus.commons.compiler.IExpressionEvaluator;
import org.codehaus.commons.compiler.ISimpleCompiler;
import org.codehaus.commons.compiler.Sandbox;
import org.codehaus.commons.nullanalysis.NotNullByDefault;
import org.codehaus.commons.nullanalysis.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import util.CommonsCompilerTestSuite;
import util.TestUtil;

/**
 * Test cases for the combination of JANINO with {@link Sandbox}.
 */
@RunWith(Parameterized.class) public
class SandboxTest extends CommonsCompilerTestSuite {

    private static final Permissions NO_PERMISSIONS = new Permissions();
    static {

        // Initialize a few classes before using NO_PERMISSIONS...
        try { InetAddress.getLocalHost(); } catch (UnknownHostException e) { throw new ExceptionInInitializerError(e); }
        new Socket();
    }

    private static final Permissions ALL_PERMISSIONS = new Permissions();
    static { SandboxTest.ALL_PERMISSIONS.add(new AllPermission()); }

    /**
     * Get all available compiler factories for the "CompilerFactory" JUnit parameter.
     */
    @Parameters(name = "CompilerFactory={0}") public static List<Object[]>
    compilerFactories() throws Exception { return TestUtil.getCompilerFactoriesForParameters(); }

    public
    SandboxTest(ICompilerFactory compilerFactory) throws Exception { super(compilerFactory); }

    /**
     * Verifies that a trivial script works in the no-permissions sandbox.
     */
    @Test public void
    testReturnTrue() throws Exception {

        String script = "return true;";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that it is not possible to retrieve a system property.
     */
    @Test(expected = AccessControlException.class) public void
    testGetSystemProperty() throws Exception {

        String script = "System.getProperty(\"foo\"); return true;";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that it is not possible to delete a file.
     */
    @Test(expected = AccessControlException.class) public void
    testFileDelete() throws Exception {

        String script = "return new java.io.File(\"path/to/file.txt\").delete();";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that it is forbidden to list a directory.
     */
    @Test(expected = AccessControlException.class) public void
    testFileList() throws Exception {

        String script = "return new java.io.File(\"path/to/dir\").list() != null;";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that {@code .class} works in the no-permissions sandbox.
     */
    @Test public void
    testDotClass() throws Exception {

        String script = "return (System.class != null);";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that {@link Class#forName(String)} is accessible in the no-permissions sandbox.
     */
    @Test public void
    testClassForName() throws Exception {

        String script = "return (System.class.forName(\"java.lang.String\") != null);";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that {@link Class#getDeclaredField(String)} is forbidden in the no-permissions sandbox.
     */
    @Test(expected = AccessControlException.class) public void
    testDotClassGetDeclaredField() throws Exception {

        String script = "return (String.class.getDeclaredField(\"CASE_INSENSITIVE_ORDER\") != null);";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that {@link Class#getDeclaredField(String)} and {@link Field#setAccessible(boolean)} <em>are</em>
     * allowed in an <em>all-permissions</em> sandbox.
     */
    @Test public void
    testDotClassGetDeclaredFieldAllPermissions() throws Exception {

        // JRE 9 throws
        //   java.lang.reflect.InaccessibleObjectException:
        //   Unable to make field private final byte[] java.lang.String.value accessible:
        //   module java.base does not "opens java.lang" to unnamed module @291ae
        // JREs 10, 11, 12, however, are not affected!?
        if (CommonsCompilerTestSuite.JVM_VERSION == 9) return;

        String script = "String.class.getDeclaredField(\"CASE_INSENSITIVE_ORDER\").setAccessible(true); return true;";
        this.confinedScriptTest(script, SandboxTest.ALL_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that creating an {@link URLConnection} is forbidden.
     */
    @Test(expected = AccessControlException.class) public void
    testUrlConnection1() throws Exception {

        // Java 7 and 8 have a design problem (or is it a bug?): The class initializer of
        // "sun.net.www.protocol.http.HttpURLConnection" runs a privileged action:
        //
        //   static {
        //       maxRedirects = ((Integer) AccessController.doPrivileged(
        //           new GetIntegerAction("http.maxRedirects", 20)
        //       )).intValue();
        //   }
        //
        // As a consequence, "URL.openConnection()" throws an InvocationTargetException, caused by
        // an ExceptionInInitializerError, caused by an AccessControlException (instead of an
        // "AccessControlException").
        // As a suitable workaround, we initialize the "sun.net.www.protocol.http.HttpURLConnection"
        // class HERE:
        new java.net.URL("http://localhost:65000").openConnection();

        // Now for the actual test case:
        String script = (
            "return new java.net.URL(\"http://localhost:65000\").openConnection().getInputStream() != null;"
        );
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that it is forbidden to resolve host names.
     */
    @Test(expected = AccessControlException.class) public void
    testSocketToHost() throws Exception {

        String script = "return new java.net.Socket(\"localhost\", 65000) != null;";
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that it is forbidden to connect to a numeric IPv4 address.
     */
    @Test(expected = AccessControlException.class) public void
    testSocketToIpAddress() throws Exception {

        String script = (
            "return new java.net.Socket(java.net.InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }), 65000) != null;"
        );
        this.confinedScriptTest(script, SandboxTest.NO_PERMISSIONS).assertResultTrue();
    }

    /**
     * Verifies that also the {@link ISimpleCompiler} checks permissions.
     */
    @Test(expected = AccessControlException.class) public void
    testSimpleCompiler() throws Exception {

        this.confinedSimpleCompilerTest(
            "public class Foo { public static void main() { System.getProperty(\"foo\"); } }",
            "Foo",
            SandboxTest.NO_PERMISSIONS
        ).assertExecutable();
    }

    /**
     * Verifies that also the {@link IClassBodyEvaluator} checks permissions.
     */
    @Test(expected = AccessControlException.class) public void
    testClassBodyEvaluator() throws Exception {

        this.confinedClassBodyTest(
            "public static void main() { System.getProperty(\"foo\"); }",
            SandboxTest.NO_PERMISSIONS
        ).assertExecutable();
    }

    /**
     * Verifies that also the {@link IExpressionEvaluator} checks permissions.
     */
    @Test(expected = AccessControlException.class) public void
    testExpressionEvaluator() throws Exception {

        this.confinedExpressionTest(
            "System.getProperty(\"foo\")",
            SandboxTest.NO_PERMISSIONS
        ).assertExecutable();
    }

    /**
     * Verifies that subthreads can be created and execute successfully.
     */
    @Test public void
    testSubthreads() throws Exception {

        // "Thread()" does some REFLECTION, so we must allow that.
        Permissions permissions = new Permissions();
        permissions.add(new RuntimePermission("accessDeclaredMembers"));

        this.confinedScriptTest((
            ""
            + "final Object[] result = new Object[1];\n"
            + "Thread t = new Thread() {\n"
            + "    @Override public void run() { result[0] = \"howdy\"; }\n"
            + "};\n"
            + "t.start();\n"
            + "t.join();\n"
            + "return \"howdy\".equals(result[0]);\n"
        ), permissions).assertResultTrue();
    }

    /**
     * Verifies that also code declared in a subthread is subject to confinement.
     */
    @Test(expected = AccessControlException.class) public void
    testSubthreadConfinement() throws Exception {

        // "Thread()" does some REFLECTION, so we must allow that.
        Permissions permissions = new Permissions();
        permissions.add(new RuntimePermission("accessDeclaredMembers"));

        this.confinedScriptTest((
            ""
            + "final Object[] result = new Object[1];\n"
            + "Thread t = new Thread() {\n"
            + "    @Override public void run() {\n"
            + "        try {\n"
            + "            result[0] = new java.io.File(\"path/to/dir\").list();\n"
            + "        } catch (Exception e) {\n"
            + "            result[0] = e;\n"
            + "        }\n"
            + "    }\n"
            + "};\n"
            + "t.start();\n"
            + "t.join();\n"
            + "if (result[0] instanceof Exception) throw (Exception) result[0];\n"
            + "return result[0] == null;\n"
        ), permissions).assertResultTrue();
    }

    // ====================================== END OF TEST CASES ======================================

    /**
     * Creates and returns a {@link ScriptTest} object that executes scripts in a {@link Sandbox} with the given
     * <var>permissions</var>.
     */
    private ScriptTest
    confinedScriptTest(String script, final PermissionCollection permissions) throws Exception {

        final Sandbox sandbox = new Sandbox(permissions);

        return new ScriptTest(script) {

            @Override protected void
            cook() throws Exception {
                this.scriptEvaluator.setThrownExceptions(new Class<?>[] { Exception.class });
                super.cook();
            }

            @Override @Nullable protected Object
            execute() throws Exception {

                return sandbox.confine(new PrivilegedExceptionAction<Object>() {
                    @Override public Object run() throws Exception { return execute2(); }
                });
            }

            @NotNullByDefault(false) private Object
            execute2() throws Exception { return super.execute(); }
        };
    }

    /**
     * Creates and returns a {@link SimpleCompilerTest} object that executes the {@code public static void main()}
     * method of the named class in a {@link Sandbox} with the given <var>permissions</var>.
     */
    private SimpleCompilerTest
    confinedSimpleCompilerTest(
        String                     compilationUnit,
        String                     className,
        final PermissionCollection permissions
    ) throws Exception {

        final Sandbox sandbox = new Sandbox(permissions);

        return new SimpleCompilerTest(compilationUnit, className) {

            @NotNullByDefault(false) private Object
            execute2() throws Exception { return super.execute(); }

            @Override @Nullable protected Object
            execute() throws Exception {

                return sandbox.confine(new PrivilegedExceptionAction<Object>() {
                    @Override public Object run() throws Exception { return execute2(); }
                });
            }
        };
    }

    /**
     * Creates and returns a {@link ClassBodyTest} object that executes the {@code public
     * static void main()} method in a {@link Sandbox} with the given <var>permissions</var>.
     */
    private ClassBodyTest
    confinedClassBodyTest(String classBody, PermissionCollection permissions) throws Exception {

        final Sandbox sandbox = new Sandbox(permissions);

        return new ClassBodyTest(classBody) {

            @NotNullByDefault(false) private Object
            execute2() throws Exception { return super.execute(); }

            @Override @Nullable protected Object
            execute() throws Exception {

                return sandbox.confine(new PrivilegedExceptionAction<Object>() {
                    @Override public Object run() throws Exception { return execute2(); }
                });
            }
        };
    }

    /**
     * Creates and returns an {@link ExpressionTest} object that evaluates its subject expression
     * in a {@link Sandbox} with the given <var>permissions</var>.
     */
    private ExpressionTest
    confinedExpressionTest(String expression, PermissionCollection permissions) throws Exception {

        final Sandbox sandbox = new Sandbox(permissions);

        return new ExpressionTest(expression) {

            @NotNullByDefault(false) private Object
            execute2() throws Exception { return super.execute(); }

            @Override @Nullable protected Object
            execute() throws Exception {

                return sandbox.confine(new PrivilegedExceptionAction<Object>() {
                    @Override public Object run() throws Exception { return execute2(); }
                });
            }
        };
    }
}