Skip to content
Advertisement

How to prevent reflection access only to certain classes

I am developing some licensing code, so I would like to avoid access to one or two classes through reflection.

I have tried implement a SecurityManager but with no success.

I found that another option would be to define my own policy file but that requires me to set it on JVM launch, and that defeats my purpose, I would like to be able to set either a SecurityManager that prevents access to the specific License classes, or a policy to do so in runtime.

Advertisement

Answer

Notes & Disclaimers

  • The answer assumes that you are in control of, or otherwise trust in, the environment; unless this is the case, applying it will be utterly ineffectual.
  • The answer is relevant for Java SE <= 8. The code provided is unlikely to work as intended from 9 onwards, and the Module System has reduced the number of cases where use of the security framework is actually necessary in order to achieve isolation.
  • Code is (as most things on the internet) provided “as-is”.

Prerequisites

Ensure any untrusted code is isolated from trusted code. Isolation in this context means:

  • separation between ProtectionDomains (two domains of two code bases of two different levels of “trustworthiness” should have non-equal, non-implying CodeSources), and
  • packages, and
  • ClassLoaders.

While the first is generally sufficient, in cases when the line between the concerns of authorization and accessibility is blurred, as is the case with reflection, the latter two points are necessitated as well. I’ll leave the details of addressing this to you. And since we’re here, a note on terminology: since the JDK doesn’t consistently authorize client code against the same of the aforementioned attributes, I’m simply going to be referring to code exercising any of the three at times of authorization decisions as an isolation unit.


Option A – Restricting package access

Perhaps the simplest solution is to prevent untrusted code from accessing the particular package at all:

  • Have a common (base) package for sensitive code.
  • Declare it as restricted, either via the package.access property in <JAVA_HOME>/lib/security/java.security, or programmatically, by appending the package name to the corresponding Security Property, by means of java.security.Security#setProperty.
  • Have clients access your sensitive code indirectly, via a trusted intermediary of yours, located outside of the restricted package. That class should be granted RuntimePermission "accessClassInPackage." + restrictedPackageName.

But there’s a catch—you also want untrusted code to have full reflective access to “the rest of the universe”, which is by no means possible without also implicitly granting it access to bring the sandbox to its knees, if it so pleases, by, for instance, reassigning System's SecurityManager field. Which leads us to…

Option B – Selectively denying reflective access

Goals

  • Grant or deny reflective access on a per-operation basis, based on attributes such as:
    • The kind of reflected member (field, method, etc.).
    • The target package.
    • The target class.
    • The member’s name.
    • Insert further of your own.
  • Model a Permission class after the above, which the default Policy configuration can express, and the default SecurityManager can enforce.

Non-goals

  • Automagically prevent privilege escalation resulting from, e.g., code privileged to reflectively invoke some method taking advantage of that method execution’s side effects.
  • Enforce such constraints within a single isolation unit.
  • Care about performance.

I’ve played with the above a bit, and here’s the result, which is anything but perfect or even bug-free, but hopefully sufficient to demonstrate the idea.

Brief overview (refer to Javadoc for details)

  • SelectiveReflectPermission is the permission to grant to untrusted code. While incapable of accommodating for further authorization attributes in its current form, modifying it to include them should be fairly straightforward.
  • AllUnsafePermission expresses a set of privileges that must not be granted to untrusted code (unless sandbox integrity is irrelevant).
  • DeniedPermission and DenyingPolicy are the same classes given here, with small modifications of the former so as to allow wrapping of “target” permissions having a no-arg constructor. These classes only add the convenience of expressing “deny-like” rules in the policy configuration, without otherwise being essential.
  • ReflectionGatekeeper delegates to some of the most frequently used parts of the reflection API, after checking for SelectiveReflectPermission. The same logic could be embedded in a custom SecurityManager‘s relevant methods (primarily #checkPermission(Permission)) invoked by core reflection, which client code would very much appreciate as opposed to having to use of a different API. But (there’s always a “but”) I chose against that option, since it would require brittle “caller-sensitive” code to be introduced to the implementation, which would have to collect the needed authorization attributes by investigating the call stack, also accounting for recursive reflective invocations, as well as invocations by multiple threads… leading to something astonishingly close to a pre-J2SE 1.2 security manager. Furthermore, as a matter of taste, I prefer the manager to contain as little actual authorization logic as possible—ideally it would merely serve as an AccessController, and, ultimately, Policy delegate, without shortcuts or loopholes.

Usage

Have either some kind of application launcher, or the initialization logic of your sensitive components themselves establish security (and fail unless successful); this could look as follows:

package com.example.trusted.launcher;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Policy;
import java.util.Collections;

import com.example.trusted.security.DenyingPolicy;

public class Launcher {

    public static void main(String... args) throws Exception {

        /*
         * Get the actual main application class, which has hopefully been loaded by a different ClassLoader,
         * and resides in a distinct package and ProtectionDomain.
         */
        Class<?> realMain = getMainFromArgs(args);

        // install a Policy and SecurityManager

        /*
         * To avoid having to administer .policy configurations at the file system, you could bundle a
         * template with your app/lib, replace any CodeSource URLs you don't know until runtime, temp-save
         * to the file system (or use some transient URLStreamHandler) and "feed" the corresponding URL to
         * the Policy provider. Or better yet, if you can spare the time, implement your own Policy provider
         * as a mutable data structure.
         */
        String policyConfig = new String(
                Files.readAllBytes(Paths.get(Launcher.class.getResource("policy_template.txt").toURI())),
                StandardCharsets.UTF_8);
    
        // replace any CodeSource URL placeholders (e.g. with realMain's cs URL)
        policyConfig = adjustPolicyConfig(policyConfig);
    
        // temp-save it and hand it over to Policy
        Path tmpPolicyFile = Files.createTempFile("policy", ".tmp");
        Files.write(tmpPolicyFile, Collections.singletonList(policyConfig));
    
        // leading equals sign ensures only the indicated config gets loaded
        System.setProperty("java.security.policy", "=" + tmpPolicyFile.toUri());
    
        // unnecessary if you don't care about deny rules
        Policy.setPolicy(new DenyingPolicy());

        System.setSecurityManager(new SecurityManager());
    
        Files.delete(tmpPolicyFile);
    
        // filter args and call real main
        invokeMain(realMain, args);
    }

    // ...

}

…while a potential policy configuration template could be:

// note: curly braces are MessageFormat-escaped

// ---

// trusted code
grant codeBase "{0}" '{'

    permission java.security.AllPermission;

'}';

// sandboxed code
grant codeBase "{1}" '{'
    
    // all permissions...
    permission java.security.AllPermission;

    // ...save for unsafe ones
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.AllUnsafePermission";

    // ...with global reflective access via ReflectionGatekeeper
    permission com.example.trusted.security.SelectiveReflectPermission;

    // ...with the exception of system code and our own com.example.trusted package
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!sun.*!*!*:*";
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!com.sun.*!*!*:*";
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!com.oracle.*!*!*:*";
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!net.java.*!*!*:*";
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!java.*!*!*:*";
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!javax.*!*!*:*";
    // currently it's not possible to express both a base package _and_ its sub-packages in a single permission
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!com.example.trusted!*!*:*";
    permission com.example.trusted.security.DeniedPermission "com.example.trusted.security.SelectiveReflectPermission:*!com.example.trusted.*!*!*:*";

'}';

With those two pieces in place, some class mapped to domain {1} will not be able to invoke a core reflection method on a Class outside of its isolation unit; but it will be able to use the ReflectionGatekeeper for the equivalent, in all cases except those explicitly denied.

How about Java SE > 8? (addition as per the comments)

With the introduction of the Module System in Java 9, accessibility was intended to become the concern of each individual module as opposed to one centrally manageable; refer to usage for provides / exports / opens as either module-info.java directives, java launch options or runtime invocations for the essentials. Centralized and/or more dynamic administration remains a possibility in different ways (instrumentation, custom module loading or ditching modules altogether in favor of a home-grown system).

The centralized SM-based approach given above remains to some extent applicable in recent versions, with potential caveats involving the built-in JDK class loaders having since ceased being URLClassLoaders as well as modular JARs utilizing a distinct URL scheme. It is however nowhere near as useful as it once was, given that the prevalent use case of preventing application or library code from reflectively accessing unsafe JDK internals (or any internals for that matter really) is already addressed by the system — nothing inside a module, A, is accessible from the outside world unless explicitly declared or rendered so at runtime by A itself. Last but no least it must be noted that the SM was terminally deprecated in Java 17.

Advertisement