blob: 5e8945a93d5b70ebb7ba5e9f99fabbb4182240bb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package freemarker.ext.beans;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import freemarker.log.Logger;
import freemarker.template.ObjectWrapper;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.NullArgumentException;
/**
* Whitelist-based member access policy, that is, only members that you have explicitly whitelisted will be accessible.
* The whitelist content is application specific, and can be significant work to put together, but it's the only way
* you can achieve any practical safety if you don't fully trust the users who can edit templates. Of course, this only
* can deal with the {@link ObjectWrapper} aspect of safety; please check the Manual to see what else is needed. Also,
* since this is related to security, read the documentation of {@link MemberAccessPolicy}, to know about the
* pitfalls and edge cases related to {@link MemberAccessPolicy}-es in general.
*
* <p>There are two ways you can add members to the whitelist:
* <ul>
* <li>Via a list of member selectors passed to the constructor
* <li>Via {@link TemplateAccessible} annotation
* </ul>
*
* <p>When a member is whitelisted, it's identified by the following data (with the example of
* {@code com.example.MyClass.myMethod(int, int)} being whitelisted):
* <ul>
* <li>Upper bound class ({@code com.example.MyClass} in the example)
* <li>Member name ({@code myMethod} in the example), except for constructors where it's unused
* <li>Parameter types ({@code int, int} in the example), except for fields where it's unused
* </ul>
*
* <p>Once you have whitelisted a member in the upper bound class, it will be automatically whitelisted in all
* subclasses of that, even if the whitelisted member is a field or constructor (which doesn't support overriding, but
* it will be treated as such if the field name or constructor parameter types match).
* It's called "upper bound" class, because the member will only be whitelisted in classes that are {@code instanceof}
* the upper bound class. That restriction stands even if the member was inherited from another class or
* interface, and it wasn't even overridden in the upper bound class; the member won't be whitelisted in the
* class/interface where it was inherited from, if that type is more generic than the upper bound class.
*
* <p>Note that the return type of methods aren't used in any way. So if you whitelist {@code myMethod(int, int)}, and
* it has multiple variants with different return types (which is possible on the bytecode level), then you have
* whitelisted all variants of it.
*
* @since 2.3.30
*/
public class WhitelistMemberAccessPolicy implements MemberAccessPolicy {
private static final Logger LOG = Logger.getLogger("freemarker.beans");
private final MethodMatcher methodMatcher;
private final ConstructorMatcher constructorMatcher;
private final FieldMatcher fieldMatcher;
/**
* A condition that matches some type members. See {@link WhitelistMemberAccessPolicy} documentation for more.
* Exactly one of these will be non-{@code null}:
* {@link #getMethod()}, {@link #getConstructor()}, {@link #getField()}, {@link #getException()}.
*
* @since 2.3.30
*/
public final static class MemberSelector {
private final Class<?> upperBoundType;
private final Method method;
private final Constructor<?> constructor;
private final Field field;
private final Exception exception;
private final String exceptionMemberSelectorString;
/**
* Use if you want to match methods similar to the specified one, in types that are {@code instanceof} of
* the specified upper bound type. When methods are matched, only the name and the parameter types matter.
*/
public MemberSelector(Class<?> upperBoundType, Method method) {
NullArgumentException.check("upperBoundType", upperBoundType);
NullArgumentException.check("method", method);
this.upperBoundType = upperBoundType;
this.method = method;
this.constructor = null;
this.field = null;
this.exception = null;
this.exceptionMemberSelectorString = null;
}
/**
* Use if you want to match constructors similar to the specified one, in types that are {@code instanceof} of
* the specified upper bound type. When constructors are matched, only the parameter types matter.
*/
public MemberSelector(Class<?> upperBoundType, Constructor<?> constructor) {
NullArgumentException.check("upperBoundType", upperBoundType);
NullArgumentException.check("constructor", constructor);
this.upperBoundType = upperBoundType;
this.method = null;
this.constructor = constructor;
this.field = null;
this.exception = null;
this.exceptionMemberSelectorString = null;
}
/**
* Use if you want to match fields similar to the specified one, in types that are {@code instanceof} of
* the specified upper bound type. When fields are matched, only the name matters.
*/
public MemberSelector(Class<?> upperBoundType, Field field) {
NullArgumentException.check("upperBoundType", upperBoundType);
NullArgumentException.check("field", field);
this.upperBoundType = upperBoundType;
this.method = null;
this.constructor = null;
this.field = field;
this.exception = null;
this.exceptionMemberSelectorString = null;
}
/**
* Used to store the result of a parsing that's failed for a reason that we can skip on runtime (typically,
* when a missing class or member was referred).
*
* @param upperBoundType {@code null} if resolving the upper bound type itself failed.
* @param exception Not {@code null}
* @param exceptionMemberSelectorString Not {@code null}; the selector whose resolution has failed, used in
* the log message.
*/
public MemberSelector(Class<?> upperBoundType, Exception exception, String exceptionMemberSelectorString) {
NullArgumentException.check("exception", exception);
NullArgumentException.check("exceptionMemberSelectorString", exceptionMemberSelectorString);
this.upperBoundType = upperBoundType;
this.method = null;
this.constructor = null;
this.field = null;
this.exception = exception;
this.exceptionMemberSelectorString = exceptionMemberSelectorString;
}
/**
* Maybe {@code null} if {@link #getException()} is non-{@code null}.
*/
public Class<?> getUpperBoundType() {
return upperBoundType;
}
/**
* Maybe {@code null};
* set if the selector matches methods similar to the returned one, and there was no exception.
*/
public Method getMethod() {
return method;
}
/**
* Maybe {@code null};
* set if the selector matches constructors similar to the returned one, and there was no exception.
*/
public Constructor<?> getConstructor() {
return constructor;
}
/**
* Maybe {@code null};
* set if the selector matches fields similar to the returned one, and there was no exception.
*/
public Field getField() {
return field;
}
/**
* Maybe {@code null}
*/
public Exception getException() {
return exception;
}
/**
* Maybe {@code null}
*/
public String getExceptionMemberSelectorString() {
return exceptionMemberSelectorString;
}
/**
* Parses a member selector that was specified with a string.
*
* @param classLoader
* Used to resolve class names in the member selectors. Generally you want to pick a class that belongs to
* you application (not to a 3rd party library, like FreeMarker), and then call
* {@link Class#getClassLoader()} on that. Note that the resolution of the classes is not lazy, and so the
* {@link ClassLoader} won't be stored after this method returns.
* @param memberSelectorString
* Describes the member (method, constructor, field) which you want to whitelist. Starts with the full
* qualified name of the member, like {@code com.example.MyClass.myMember}. Unless it's a field, the
* name is followed by comma separated list of the parameter types inside parentheses, like in
* {@code com.example.MyClass.myMember(java.lang.String, boolean)}. The parameter type names must be
* also full qualified names, except primitive type names. Array types must be indicated with one or
* more {@code []}-s after the type name. Varargs arguments shouldn't be marked with {@code ...}, but with
* {@code []}. In the member name, like {@code com.example.MyClass.myMember}, the class refers to the so
* called "upper bound class". Regarding that and inheritance rules see the class level documentation.
*
* @return The {@link MemberSelector}, which might has non-{@code null} {@link MemberSelector#exception}.
*/
public static MemberSelector parse(String memberSelectorString, ClassLoader classLoader) {
if (memberSelectorString.contains("<") || memberSelectorString.contains(">")
|| memberSelectorString.contains("...") || memberSelectorString.contains(";")) {
throw new IllegalArgumentException(
"Malformed whitelist entry (shouldn't contain \"<\", \">\", \"...\", or \";\"): "
+ memberSelectorString);
}
String cleanedStr = memberSelectorString.trim().replaceAll("\\s*([\\.,\\(\\)\\[\\]])\\s*", "$1");
int postMemberNameIdx;
boolean hasArgList;
{
int openParenIdx = cleanedStr.indexOf('(');
hasArgList = openParenIdx != -1;
postMemberNameIdx = hasArgList ? openParenIdx : cleanedStr.length();
}
final int postClassDotIdx = cleanedStr.lastIndexOf('.', postMemberNameIdx);
if (postClassDotIdx == -1) {
throw new IllegalArgumentException("Malformed whitelist entry (missing dot): " + memberSelectorString);
}
Class<?> upperBoundClass;
String upperBoundClassStr = cleanedStr.substring(0, postClassDotIdx);
if (!isWellFormedClassName(upperBoundClassStr)) {
throw new IllegalArgumentException("Malformed whitelist entry (malformed upper bound class name): "
+ memberSelectorString);
}
try {
upperBoundClass = classLoader.loadClass(upperBoundClassStr);
} catch (ClassNotFoundException e) {
return new MemberSelector(null, e, cleanedStr);
}
String memberName = cleanedStr.substring(postClassDotIdx + 1, postMemberNameIdx);
if (!isWellFormedJavaIdentifier(memberName)) {
throw new IllegalArgumentException(
"Malformed whitelist entry (malformed member name): " + memberSelectorString);
}
if (hasArgList) {
if (cleanedStr.charAt(cleanedStr.length() - 1) != ')') {
throw new IllegalArgumentException("Malformed whitelist entry (missing closing ')'): "
+ memberSelectorString);
}
String argsSpec = cleanedStr.substring(postMemberNameIdx + 1, cleanedStr.length() - 1);
StringTokenizer tok = new StringTokenizer(argsSpec, ",");
int argCount = tok.countTokens();
Class<?>[] argTypes = new Class[argCount];
for (int i = 0; i < argCount; i++) {
String argClassName = tok.nextToken();
int arrayDimensions = 0;
while (argClassName.endsWith("[]")) {
arrayDimensions++;
argClassName = argClassName.substring(0, argClassName.length() - 2);
}
Class<?> argClass;
Class<?> primArgClass = ClassUtil.resolveIfPrimitiveTypeName(argClassName);
if (primArgClass != null) {
argClass = primArgClass;
} else {
if (!isWellFormedClassName(argClassName)) {
throw new IllegalArgumentException(
"Malformed whitelist entry (malformed argument class name): " + memberSelectorString);
}
try {
argClass = classLoader.loadClass(argClassName);
} catch (ClassNotFoundException e) {
return new MemberSelector(upperBoundClass, e, cleanedStr);
} catch (SecurityException e) {
return new MemberSelector(upperBoundClass, e, cleanedStr);
}
}
argTypes[i] = ClassUtil.getArrayClass(argClass, arrayDimensions);
}
try {
return memberName.equals(upperBoundClass.getSimpleName())
? new MemberSelector(upperBoundClass, upperBoundClass.getConstructor(argTypes))
: new MemberSelector(upperBoundClass, upperBoundClass.getMethod(memberName, argTypes));
} catch (NoSuchMethodException e) {
return new MemberSelector(upperBoundClass, e, cleanedStr);
} catch (SecurityException e) {
return new MemberSelector(upperBoundClass, e, cleanedStr);
}
} else {
try {
return new MemberSelector(upperBoundClass, upperBoundClass.getField(memberName));
} catch (NoSuchFieldException e) {
return new MemberSelector(upperBoundClass, e, cleanedStr);
} catch (SecurityException e) {
return new MemberSelector(upperBoundClass, e, cleanedStr);
}
}
}
/**
* Convenience method to parse all member selectors in the collection; see {@link #parse(String, ClassLoader)}.
*/
public static List<MemberSelector> parse(Collection<String> memberSelectors,
ClassLoader classLoader) {
List<MemberSelector> parsedMemberSelectors = new ArrayList<MemberSelector>(memberSelectors.size());
for (String memberSelector : memberSelectors) {
parsedMemberSelectors.add(parse(memberSelector, classLoader));
}
return parsedMemberSelectors;
}
}
public WhitelistMemberAccessPolicy(Collection<MemberSelector> memberSelectors) {
methodMatcher = new MethodMatcher();
constructorMatcher = new ConstructorMatcher();
fieldMatcher = new FieldMatcher();
for (MemberSelector memberSelector : memberSelectors) {
Class<?> upperBoundClass = memberSelector.upperBoundType;
if (memberSelector.exception != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Member selector ignored due to error: " + memberSelector.getExceptionMemberSelectorString(),
memberSelector.exception);
}
} else if (memberSelector.constructor != null) {
constructorMatcher.addMatching(upperBoundClass, memberSelector.constructor);
} else if (memberSelector.method != null) {
methodMatcher.addMatching(upperBoundClass, memberSelector.method);
} else if (memberSelector.field != null) {
fieldMatcher.addMatching(upperBoundClass, memberSelector.field);
} else {
throw new AssertionError();
}
}
}
public ClassMemberAccessPolicy forClass(final Class<?> contextClass) {
return new ClassMemberAccessPolicy() {
public boolean isMethodExposed(Method method) {
return methodMatcher.matches(contextClass, method)
|| _MethodUtil.getInheritableAnnotation(contextClass, method, TemplateAccessible.class) != null;
}
public boolean isConstructorExposed(Constructor<?> constructor) {
return constructorMatcher.matches(contextClass, constructor)
|| _MethodUtil.getInheritableAnnotation(contextClass, constructor, TemplateAccessible.class)
!= null;
}
public boolean isFieldExposed(Field field) {
return fieldMatcher.matches(contextClass, field)
|| _MethodUtil.getInheritableAnnotation(contextClass, field, TemplateAccessible.class) != null;
}
};
}
private static boolean isWellFormedClassName(String s) {
if (s.length() == 0) {
return false;
}
int identifierStartIdx = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (i == identifierStartIdx) {
if (!Character.isJavaIdentifierStart(c)) {
return false;
}
} else if (c == '.' && i != s.length() - 1) {
identifierStartIdx = i + 1;
} else {
if (!Character.isJavaIdentifierPart(c)) {
return false;
}
}
}
return true;
}
private static boolean isWellFormedJavaIdentifier(String s) {
if (s.length() == 0) {
return false;
}
if (!Character.isJavaIdentifierStart(s.charAt(0))) {
return false;
}
for (int i = 1; i < s.length(); i++) {
if (!Character.isJavaIdentifierPart(s.charAt(i))) {
return false;
}
}
return true;
}
}