Skip to content

Commit

Permalink
Added ACL-based restrictions on what classes can be referenced in exp…
Browse files Browse the repository at this point in the history
…ressions
  • Loading branch information
danielfernandez committed Jan 17, 2022
1 parent b4051d3 commit ea67148
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 1 deletion.
Expand Up @@ -21,7 +21,9 @@

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import ognl.ClassResolver;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
Expand All @@ -35,6 +37,7 @@
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.expression.IExpressionObjects;
import org.thymeleaf.standard.util.StandardExpressionUtils;
import org.thymeleaf.util.ExpressionUtils;

/**
* <p>
Expand Down Expand Up @@ -66,6 +69,8 @@ public final class OGNLVariableExpressionEvaluator
OGNLContextPropertyAccessor.RESTRICT_REQUEST_PARAMETERS);


private static ThymeleafACLClassResolver CLASS_RESOLVER = new ThymeleafACLClassResolver();

private final boolean applyOGNLShortcuts;


Expand Down Expand Up @@ -322,7 +327,7 @@ private static Object executeExpression(

// We create the OgnlContext here instead of just sending the Map as context because that prevents OGNL from
// creating the OgnlContext empty and then setting the context Map variables one by one
final OgnlContext ognlContext = new OgnlContext(context);
final OgnlContext ognlContext = new OgnlContext(CLASS_RESOLVER, null, null, context);
return ognl.Ognl.getValue(parsedExpression, ognlContext, root);

}
Expand All @@ -345,5 +350,61 @@ private static final class ComputedOGNLExpression {
}


static final class ThymeleafACLClassResolver implements ClassResolver {

private final ClassResolver classResolver;

public ThymeleafACLClassResolver() {
super();
this.classResolver = new ThymeleafDefaultClassResolver();
}

@Override
public Class<?> classForName(final String className, final Map context) throws ClassNotFoundException {
if (className != null && !ExpressionUtils.isTypeAllowed(className)) {
throw new TemplateProcessingException(
String.format(
"Access is forbidden for type '%s' in Thymeleaf expressions. " +
"Blacklisted packages are: %s. Whitelisted classes are: %s.",
className, ExpressionUtils.getBlacklist(), ExpressionUtils.getWhitelist()));
}
return this.classResolver.classForName(className, context);
}

}


/*
* We need to implement this instead of directly using OGNL's DefaultClassResolver because OGNL's
* will always try to prepend "java.lang." to classes that it cannot find, which in our case is dangerous. *
* Other than that, the code in this class is the same as "ognl.DefaultClassResolver".
*/
static final class ThymeleafDefaultClassResolver implements ClassResolver {

private final ConcurrentHashMap<String, Class> classes = new ConcurrentHashMap<String, Class>(101);

public ThymeleafDefaultClassResolver() {
super();
}

public Class classForName(final String className, final Map context) throws ClassNotFoundException {
Class result = this.classes.get(className);
if (result != null) {
return result;
}
try {
result = toClassForName(className);
} catch (ClassNotFoundException e) {
throw e;
}
this.classes.putIfAbsent(className, result);
return result;
}

private Class toClassForName(String className) throws ClassNotFoundException {
return Class.forName(className);
}

}

}
92 changes: 92 additions & 0 deletions src/main/java/org/thymeleaf/util/ExpressionUtils.java
@@ -0,0 +1,92 @@
/*
* =============================================================================
*
* Copyright (c) 2011-2022, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed 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 org.thymeleaf.util;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class ExpressionUtils {

private static final Set<String> BLACKLISTED_CLASS_NAME_PREFIXES =
new HashSet<String>(Arrays.asList(
"java", "javax", "jakarta", "org.ietf.jgss", "org.omg", "org.w3c.dom", "org.xml.sax"));


// The whole "java.time.*" package will also be whitelisted
private static final Set<String> WHITELISTED_JAVA_CLASS_NAMES =
new HashSet<String>(Arrays.asList(
"java.lang.Boolean", "java.lang.Byte", "java.lang.Character", "java.lang.Double",
"java.lang.Enum", "java.lang.Float", "java.lang.Integer", "java.lang.Long", "java.lang.Math",
"java.lang.Number", "java.lang.Short", "java.lang.String",
"java.math.BigDecimal", "java.math.BigInteger", "java.math.RoundingMode",
"java.util.List", "java.util.Map", "java.util.Map.Entry", "java.util.Set",
"java.util.ArrayList", "java.util.LinkedList", "java.util.HashMap", "java.util.LinkedHashMap",
"java.util.HashSet", "java.util.LinkedHashSet", "java.util.Iterator", "java.util.Enumeration",
"java.util.Locale", "java.util.Properties", "java.util.Date", "java.util.Calendar",
"java.util.Collection"));



public static boolean isTypeAllowed(final String typeName) {
Validate.notNull(typeName, "Type name cannot be null");
final int i0 = typeName.indexOf('.');
if (i0 >= 0) {
final String package0 = typeName.substring(0, i0);
if ("java".equals(package0)) { // This is the only prefix that allows whitelisting
return WHITELISTED_JAVA_CLASS_NAMES.contains(typeName);
} else if ("javax".equals(package0) || "jakarta".equals(package0)) {
return false;
} else if ("org".equals(package0)) {
if (typeName.startsWith("org.ietf.jgss")
|| typeName.startsWith("org.omg")
|| typeName.startsWith("org.w3c.dom")
|| typeName.startsWith("org.xml.sax")) {
return false;
}
}
return true;
}
// This is safe assuming we have disabled the capability of calling "java.lang" classes without package
return true;
}



public static List<String> getBlacklist() {
return BLACKLISTED_CLASS_NAME_PREFIXES.stream()
.sorted().map(p -> String.format("%s.*", p)).collect(Collectors.toList());
}

public static List<String> getWhitelist() {
return Stream.concat(WHITELISTED_JAVA_CLASS_NAMES.stream(), Stream.of("java.time.*"))
.sorted().collect(Collectors.toList());
}


private ExpressionUtils() {
super();
}

}

0 comments on commit ea67148

Please sign in to comment.