You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 7 Next »


Introduction

The query engine allows applications to access public field values or execute methods on the objects stored within a Geode Region. If a field used in query is not declared as public within the object, the query engine automatically tries to get the value for the field using accessor methods. Prior to the release of Geode 1.3.0, OQL used to allow any method invocation on objects present in the member’s classpath, including setters and, through the usage of Java Reflection, internal Geode and even JDK or external library (Spring, Tomcat, etc.) methods. This could impact the integrity of the data, the region, and the platform on which Geode is running.

As part of GEODE-3247, several options were analysed and, after considering the wealth of security holes and the difficulty of determining which methods deployed by the developer were intended to be available for queries and which were not, the decision was made to tighten up the Security and, by default, disallow any method call not explicitly added to an acceptance list.

After the solution was released and deployed by several users, the feedback wasn’t exactly the best and the overall feeling was that enabling this feature made OQL unusable. Our customers ended up changing the access of the attributes on their domain model to be public so the Field can be directly accessed by the query engine without requiring a method invocation (we shouldn't impose this restriction on the data model!!) or disabling OQL security altogether through the system property QueryService.allowUntrustedMethodInvocation.

We certainly know that malicious users can write complex OQL expression and potentially compromise the entire system, but just disallowing everything is not the best solution; allowing normal method invocations on the user’s domain model should be allowed and easily achieved. That said, we know also that we can’t automatically define the trust boundaries for deployed code, and that trusting by default all code deployed by a user with enough privileges is not feasible either: the user might have deployed the code but it might not want that code to be invoked through OQL by a READ only user. What we can do, however, is to make the integration easier and provide our customers with the right tools and means to easily decide which methods should be allowed and which ones should be denied.

As a summary, the current OQL security implementation is too restrictive and turns the whole OQL feature almost unusable when security is enabled. The experience has also demonstrated that there is no "One size fits all" solution for this requirement, so the goal of this document is to analyze possible options to loosen the security restrictions and facilitate the developer’s experience, along with taking a final step to decide which option to pursue once the community has approved the suggestions.

From now on and in the context of this document, Method Authorization Implementation refers to the actual mechanism/execution of the logic involved in determining whether a specific method should be allowed or denied during the execution of a particular query.

Goals

  • Method Authorization Implementation should be pluggable.
  • Method Authorization Implementation should be “on” by default when Security is enabled at cluster level.
  • Geode should prevent RCE (Remote Code Execution) exploits and other vulnerabilities in OQL expressions.
  • Users should be able to invoke methods on domain classes (present on the system classpath or deployed through gfsh) as part of OQL, relatively easy and with little to no configuration changes.

Requirements

  • Developers/Operators should be allowed to provide their own Method Authorization implementation.

  • The Method Authorization Implementation should be stored and retrieved through the cluster configuration service.

  • The current system property QueryService.allowUntrustedMethodInvocation should be kept but marked as deprecated.
  • The Method Authorization Implementation should be configurable/exchangeable in runtime (doesn’t need to be instantaneous update, eventual consistency is tolerable).

Current Implementation

MethodInvocationAuthorizer

The single internal interface responsible of verifying whether a method can be invoked on a certain object or not:

public interface MethodInvocationAuthorizer {
void authorizeMethodInvocatioon(Method method, Object target);
}

The implementation first checks whether the method is part of the allowed list, if it is, proceeds to check whether the user has the required privileges to execute queries on that particular region (DATA:READ:regionName). The current approach denies everything and only allows some methods to be executed, this list can not be configured or changed in runtime, it’s hard coded and any addition/deletion requires a new release of the productThe list of currently allowed methods is attached below.

Class

Method

Object

toString, equals, compareTo

Boolean

booleanValue

Number

byteValue, intValue, doubleValue, floatValue, longValue, shortValue

Collection, Map

get, entrySet, keySet, values, getEntries, getValues, containsKey

Map.Entry

getKey, getValue

Date

after, before, getNanos, getTime

String

chartAt, codePointAt, codePointBefore, codePointCount, compareToIgnoreCase, concat, contains, contentEquals, endsWith, equalsIgnoreCase, getBytes, hashCode, indexOf, intern, isEmpty, lastIndexOf, length, matches, offsetByCodePoints, replace, replaceAll, replaceFirst, split, startsWith, substring, toCharArray, toLowerCase, toUpperCase, trim

There is one instance of MethodInvocationAuthorizer per QueryService: Geode creates a new one every time the user requests a QueryService from the Cache instance. Within the initialization:

  • If the SecurityManager is not enabled, or if the flag QueryService.allowUntrustedMethodInvocation is set as true, the MethodInvocationAuthorizer is created as a no-op (no security).
  • If the SecurityManager is enabled and the flag QueryService.allowUntrustedMethodInvocation is set as false (default), the RestrictedMethodInvocationAuthorizer is configured.

The method authorizeMethodInvocation is only invoked during the OQL execution through the MethodDispatch and AttributeDescriptor classes.

MethodDispatch

The class is used to execute a specific method on a target object while an OQL is being executed, is worth noticing that currently there is no context nor metadata about what the target class is or where it comes from.

As an example, within the expression user.isEnabled() the method would be isEnabled() and the target object would be the actual runtime type/class for user. The single entry point to this class is the invoke() method, which is called through CompiledOperation.evaluate(ExecutionContext context). This particular class, CompiledOperation, is just a node within the AST tree generated by the OQLParser, and it basically represents a method invocation in OQL.

Continuing with the example and assuming that the OQL contains the user.isEnabled() expression, below is a simplified pseudo code of how this is achieved in runtime:

invoke {

  methodInvocationAuthorizer.authorize("isEnabled", "user")

  return Method.invoke("user")

}

AttributeDescriptor

The class is used to read a specific attribute from a target object while an OQL is being executed, is worth noticing again that currently there is no context nor metadata about what the target class is or where it comes from.

As an example, within the expression user.name the attribute would be name and the target object would be the actual runtime type/class for user. The single entry point to this class is the read(Object target) method, which is basically invoked through CompiledPath.evaluate(ExecutionContext context). This class, CompiledPath, is just a node within the AST tree generated by the OQLParser, and it represents an identifier that follows a “dot” operator.

Assuming that the OQL is looking for user.name, below is a simplified pseudo code of how this is achieved in runtime:

read {

  if "user" instance of PdxInstance

    if "name" is a field directly accessible through the PdxInstance

      read "name" using only the PdxInstance (getRawField, getPdxField, etc)

    else

      deserialize the PDX object and get an instance of "user"

      readReflection

  else

    readReflection

}


readReflection {

  accessType = get available access strategy to "name" attribute on "user" instance through reflection

  switch (accessType):

    case Field: {

      return Field.get("user")

    }

    case Method: {

      methodInvocationAuthorizer.authorize("getName()" / "name()", "user")

      return Method.invoke(“user”)


    }

}

Proposal

Summary

Add a new top-level class, QueryInvocationAuthorizer, having the entire Method Authorization Implementation and split the current MethodInvocationAuthorizer in two different interfaces, each one having a clear and independent responsibility:

  • RegionInvocationAuthorizer
  • MethodInvocationAuthorizer

The QueryInvocationAuthorizer will use an empty implementation of both interfaces if security is disabled or the flag QueryService.allowUntrustedMethodInvocation is set as true (as it works right now). If security is enabled, the default RegionInvocationAuthorizer will be used, and a default MethodInvocationAuthorizer will be set or, if configured, the custom implementation will be used. In runtime, the RegionInvocationAuthorizer will be always invoked first; if it succeeds and the user has privileges to query the region, the configured implementation of MethodInvocationAuthorizer will be executed.

Based on the comments added to the original proposal and having in mind the current requirements, the following implementations should be provided out of the box:

  • RegexBasedMethodAuthorizer
  • DataAwareBasedMethodAuthorizer
  • JavaBeanAccessorBasedMethodAuthorizer

The three of them, by default, will only allow the methods currently allowed by RestrictedMethodInvocationAuthorizer plus whatever the internal implementation is configured to do. The JavaBeanAccessorBasedMethodAuthorizer and DataAwareBasedMethodAuthorizer cover the most common use cases and requires little to no configuration effort. For those use cases not covered, the user can choose to use the RegexBasedMethodAuthorizer, which allows them to configure which methods to allow directly through regex expressions. If none of the above is a good fit for a particular situation, the user ultimately has the option to provide its own implementation of the MethodInvocationAuthorizer interface and do whatever needed in order to allow/deny the execution of particular methods.

Implementation Details

This section is just an overview and it contains some ideas of how the proposal could be achieved, no PoC has been done so far so the implementations details might certainly change.


Class QueryInvocationAuthorizer (Internal)

Default implementation, it should verify whether the OQL can be executed on a particular region and whether the method is allowed or not by delegating to RegionInvocationAuthorizer and MethodInvocationAuthorizer respectively. 

Interface RegionQueryInvocationAuthorizer (Internal)

The default implementation should just check whether the user can execute queries on the region (DATA:READ:RegionName) and it should be invoked first in the chain (no point in checking a method if the user doesn’t even have read privileges on the region).

public interface RegionQueryInvocationAuthorizer {

  void authorizeRegionAccess(Region region);

}

Interface MethodInvocationAuthorizer (Public)

This interface is intended to be implemented by users wanting a custom authorization mechanism and by out of the box implementations as well. The interface will have only one method and it should throw NotAuthorizedException (non-checked exception) whenever it detects that the user should not be allowed to execute that particular method during the OQL execution.

public interface MethodInvocationAuthorizer  {

  void authorizeMethodInvocation(Method method, Region region) throws NotAuthorizedException;

}

Abstract Class MethodInvocationAuthorizerAdapter (Public)

Utility class that implements MethodInvocationAuthorizer and has the current RestrictedMethodInvocationAuthorizer logic embedded. Applications can use this class (inheritance) as the starting point for providing custom authorizers.

Configuration Options

Create a new QueryEngineConfig element at the CacheConfig level to contain any configuration related to OQL, including the custom MethodInvocationAuthorizer. Even though at the beginning this new configuration element will be used to configure only the MethodInvocationAuthorizer implementation, it’s worth noting that it provides a single configuration point for the whole query engine. This basically means that it can also be used in the future to allow further additions and configuration options, even replacing the current system properties used to configure the QueryService with new child elements and/or attributes.

The resulting XML element would look something like the following (the properties are just examples):

<query-engine>

 <queryVerbose>true</queryVerbose>

 <allowUntrustedMethodInvocation>false</allowUntrustedMethodInvocation>

 <method-authorizer>

   <class-name>test.Authorizer</class-name>

   <parameter name="allowedMethodsDataBaseUrl">

     <string>jdbc:mysql://myHost/allowedMethodsDatabase</string>

   </parameter>

 </method-authorizer>

</query-engine>


This new configuration element and its properties should be stored and retrieved through the cluster configuration service, and must also be modifiable through gfsh commands:

  • alter query-engine --allowUntrustedMethodInvocation=false
  • alter query-engine --method-authorizer=test.Authorizer{'param':'value'}

Out of the Box Implementations

The code below is shown only as an example, the final implementation might differ.

RegexBasedMethodAuthorizer

Methods allowed to be executed should match some regex expression(s) configured by the user, similar to what we currently do today with the PDX ReflectionBasedAutoSerializer. The implementation will have an internal structure containing already compiled Pattern instances and use them to verify the actual method that should be executed by the OQL engine, denying or allowing the execution based on the match result.


@Override

public void authorizeMethodInvocation(Method method, Region region) throws NotAuthorizedException {

 boolean matches = false;

 String methodName = method.getClass().getName() + "." + method.getName();

 Iterator<Pattern> iterator = this.patternsCache.iterator();

 while (iterator.hasNext() && !matches) {

   matches = iterator.next().matcher(methodName).matches();

 }

 throw new NotAuthorizedException(UNAUTHORIZED_STRING + methodName);

}

Advantages

  • Easy to use and configure what to allow/deny.
  • Regular expressions are standard, everyone “should know” how to use them.

Risks / Unknowns / Disadvantages

  • Performance impact?.
  • Customers still need to configure “something” (the regex).
  • Customers need to learn regex expressions, if they don't do already.
  • Operators with little Regex knowledge can accidentally allow everything, depending on which wildcards are used.

DataAwareBasedMethodAuthorizer

Allow the OQL engine to execute any method on any instance that is part of the object hierarchy inserted into the Geode Region. The idea is, basically, to allow everything as long as it has been inserted into the region by an authenticated user. It's the responsibility of the user to train operators and developers to not execute dangerous methods or mutators on their own objects (if any). Some known dangerous methods (like getClass) should be disabled by default, however.

Advantage

  • No extra configuration needed.

  • Extremely flexible, user can execute any method.

  • Solves the general use case: deploy the domain model, start executing OQL and invoking methods without further changes (other than setting this authorizer through configuration).

Risks / Unknowns / Disadvantages

  • How would it work for method invocation chain? (user.getAddress().getZipCode().getId()).

  • How to get extra metadata about the object on which the method will be executed?. With the current implementation is not possible.

  • Java Reflection is already expensive, going up through the object hierarchy to find out whether the object is part of the region or not will definitely and negatively impact performance.

JavaBeanAccessorBasedMethodAuthorizer

Allow the OQL engine to execute any method that follows the design patterns for accessor methods described in the JavaBean specification 1.01; that is, basically, allow any method starting with get or is. For extra security, only methods belonging to classes under certain packages (configured by the user) should be allowed, and some known dangerous methods (like getClass) should be disabled.


@Override

public void authorizeMethodInvocation(Method method, Region region) throws NotAuthorizedException {

 boolean matches = false;

 String methodName = method.getName().toLowerCase();

 String packageName = method.getDeclaringClass().getPackage().getName().toLowerCase();

 if ((methodName.startsWith("get") || methodName.startsWith("is")) && (!methodName.equalsIgnoreCase("getClass"))) {

   Iterator<String> iterator = this.packagesCache.iterator();

   while (iterator.hasNext() && !matches) {

     matches = iterator.next().startsWith(packageName);

   }

 }

 throw new NotAuthorizedException(UNAUTHORIZED_STRING + methodName);

}

Advantages

  • No major changes needed.

  • Solves the general use case: most customers use get*/is* as the name for accessor methods and the configurable package restricts access only to methods from the domain model.

Risks / Unknowns / Disadvantages

  • Performance impact?.

  • Customers need to configure “something” (the package).

  • Some safe accessor methods might not start with get*/is*.

  • Not every method starting with get*/is* (even on the domain model) might be safe to invoke.

Examples

This section contains some examples showing how the feature should work, once it’s implemented, for different use cases. All examples assume that the cluster is already up and running with security enabled and that the current default method authorizer is configured (nothing is allowed). For the sake of simplicity, let’s also assume that the domain model is entirely contained within packages "order.model" and "tickets.model".

Happy Path

# All classes follow the JavaBean specification 1.01 for accessor methods

# Customer deploys the jar and configures the JavaBeanAccessorBasedMethodAuthorizer

$> deploy --jar=/tmp/model-1.0.0.jar

$> alter query-engine --method-authorizer=JavaBeanAccessorBasedMethodAuthorizer{'packages' : 'order.model,tickets.model'}

Change Authorizer in Runtime

# All classes follow the JavaBean specification 1.01 for accessor methods

# Customer deploys the jar and configures the JavaBeanAccessorBasedMethodAuthorizer

$> deploy --jar=/tmp/model-1.0.0.jar

$> alter query-engine --method-authorizer=JavaBeanAccessorBasedMethodAuthorizer{'packages' : 'order.model,tickets.model'}


# Customer realizes that one developer did not follow the specification and multiple "calculateXXXX" methods exist in several classes

# These methods need to be accessed through OQL right away in production, so they configure RegexBasedMethodAuthorizer with the required regex to allow default java bean accessors (get*|is*) + methods starting with "calculate*"

alter query-engine --method-authorizer=RegexBasedMethodAuthorizer{'patterns' : 'model.*(get|is|calculate)'}


# After removing all "calculateXXXX" methods the customer deploys the new model and re-configures the JavaBeanAccessorBasedMethodAuthorizer

deploy --jar=/tmp/model-2.0.0.jar

alter query-engine --method-authorizer=JavaBeanAccessorBasedMethodAuthorizer{'packages' : 'order.model,tickets.model'}

Custom Authorizer with Hot Deploy

# Out of the box implementations are not suitable for the use case

# A method needs to be allowed on certain regions but denied on others

# Customer develops a custom method authorizer that gets the list of allowed methods per region from an external environment variable

public class EnvironmentAwareAuthorizer implements MethodInvocationAuthorizer {

  private final static String METHODS_ALLOWED_PER_REGION_KEY = "myEnvironmentVariable";

  private Map<String, List<String>> acceptList;


  private void parseEnvironment(String variableName) {

    // Parse JSON environment variable and populate internal map.

  }


  public EnvironmentAwareAuthorizer() {

    parseEnvironment(METHODS_ALLOWED_PER_REGION_KEY);

  }


  @Override

  public void authorizeMethodInvocation(Method method, Region region) throws NotAuthorizedException {

    String regionName = region.getName().toLowerCase();

    String methodName = method.getName().toLowerCase();

    List<String> methodsAllowedWithinRegion = acceptList.get(regionName);

    if ((methodsAllowedWithinRegion != null) && (methodsAllowedWithinRegion.contains(methodName))) {

      return;

    }

    throw new NotAuthorizedException(UNAUTHORIZED_STRING + methodName);

  }

}


# Customer packages the custom implementation + data-model, deploys the single JAR file and configures the custom authorizer

$> deploy --jar=/tmp/library-1.0.0.jar

$> alter query-engine --method-authorizer=io.company.EnvironmentAwareAuthorizer

Discarded Options

The following approaches were discarded for several reasons, they’re included here only for historic purposes.

PDXBasedMethodAuthorizer

We can’t invoke actual methods on objects stored as PdxInstance without deserializing them, that’s a fact, BUT we could have a Method Authorization Implementation that automatically trusts all methods invoked on objects that were deserialized from a PdxInstance.

Pros and Cons

  • Easy to implement (+).
  • No major changes needed (+).
  • Not every method on the object might be safe to execute (-).
  • Customer needs to configure PDX AND also deploy the domain model (-).

ConfigurableAcceptListMethodAuthorizer

Keep the Method Authorization Implementation details internal and only provide support to configure the list of allowed methods to the users. The approach basically implies keeping the actual behaviour and RestrictedMethodInvocationAuthorizer implementation, but improving the class to make it "mutable" in terms of configuration so users can add or remove custom methods to the list of allowed methods in runtime. The methods actually allowed should match exactly the ones to be executed by OQL, any difference will result in the method execution to be rejected by the query engine.

Pros and Cons

  • No major changes needed (+).
  • Easy to understand and configure (+).
  • Cumbersome Configuration (-).
    • Becomes a nightmare for huge data models.
    • User needs to add methods to the accepted list one by one, per class.
    • Maintenance complexity when the amount of methods to allow increases.

AnnotationBasedMethodAuthorizer

Add a new annotation to Geode (@Authorized, @OQLMethod, other?) so users can annotate within their domain model which methods are safe to invoke during OQL execution. In order to decide whether the method is allowed or denied, the implementation will need to check whether the method is annotated with the marker annotation or not.

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@interface Authorized {

}


@Override

public void authorizeMethodInvocation(Method method) throws NotAuthorizedException {

 if (!method.isAnnotationPresent(Authorized.class)) {

   throw new NotAuthorizedException(UNAUTHORIZED_STRING + method.getName());

 }

}

Pros and Cons

  • Easy to use and implement (+).
  • Annotations are becoming the standard, developers are comfortable with them (+).
  • No extra configuration needed: the deployed domain model contains what we need in runtime (+).
  • The domain model classes must be present on the server’s classpath (-).
  • An unnecessary level of coupling is added between the user’s code and Geode (-).
  • Code is not code anymore, there is embedded configuration in the domain model (-).

ResourcePermissionBasedMethodAuthorizer

Allow any method invocation for users that have the DATA:QUERY:RegionName role. This approach basically allows the operator/developer to decide which users have rights to execute any method for queries on a particular region (DATA:READ:RegionName already allows OQL execution, DATA:QUERY:RegionName will also allow method execution as part of the OQL on that particular region).

Pros and Cons

  • Easy to implement (+).
  • No extra configuration or changes needed (+).
  • Addition of new resource permission DATA:QUERY:RegionName (-).
  • Confusing. Multiple roles are required for “the same” OQL execution operation (-).



  • No labels