This is an automated email from the ASF dual-hosted git repository.
jacopoc pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new 05a0676c28 Implemented: Introduce RestrictedStaticModels to enforce
whitelist access to static methods and fields by means of the FreeMarker
"Static" shared variable
05a0676c28 is described below
commit 05a0676c28b38324649fa1ecf8f46fcb9d38eabe
Author: Jacopo Cappellato <[email protected]>
AuthorDate: Wed Mar 11 08:27:18 2026 +0100
Implemented: Introduce RestrictedStaticModels to enforce whitelist access
to static methods and fields by means of the FreeMarker "Static" shared variable
---
.../ofbiz/base/util/template/FreeMarkerWorker.java | 22 ++-
.../base/util/template/RestrictedStaticModels.java | 178 +++++++++++++++++++++
.../apache/ofbiz/entity/util/EntitySaxReader.java | 4 +-
.../config/freemarker-whitelist.properties | 159 ++++++++++++++++++
4 files changed, 357 insertions(+), 6 deletions(-)
diff --git
a/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java
b/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java
index 2a763cd386..13bcb6e77c 100644
---
a/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java
+++
b/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java
@@ -82,12 +82,28 @@ public final class FreeMarkerWorker {
private static final UtilCache<String, Template> CACHED_TEMPLATES =
UtilCache.createUtilCache("template.ftl.general", 0, 0, false);
private static final BeansWrapper DEFAULT_OFBIZ_WRAPPER = new
BeansWrapperBuilder(VERSION).build();
+ private static final TemplateHashModel DEFAULT_RESTRICTED_STATIC_MODELS =
+
RestrictedStaticModels.fromConfig(DEFAULT_OFBIZ_WRAPPER.getStaticModels(),
"freemarker-whitelist");
private static final Configuration DEFAULT_OFBIZ_CONFIG =
makeConfiguration(DEFAULT_OFBIZ_WRAPPER);
public static BeansWrapper getDefaultOfbizWrapper() {
return DEFAULT_OFBIZ_WRAPPER;
}
+ /**
+ * Returns the whitelist-restricted {@link TemplateHashModel} that backs
the
+ * {@code Static} shared variable in every FreeMarker template.
+ *
+ * <p>Use this instead of {@code
getDefaultOfbizWrapper().getStaticModels()} whenever
+ * you need to expose the {@code Static} variable to a custom FreeMarker
context, so
+ * that the same whitelist restrictions apply uniformly.
+ *
+ * @return the {@link RestrictedStaticModels} instance for the default
OFBiz wrapper
+ */
+ public static TemplateHashModel getRestrictedStaticModels() {
+ return DEFAULT_RESTRICTED_STATIC_MODELS;
+ }
+
public static Configuration newConfiguration() {
return new Configuration(VERSION);
}
@@ -96,10 +112,10 @@ public final class FreeMarkerWorker {
Configuration newConfig = newConfiguration();
newConfig.setObjectWrapper(wrapper);
- TemplateHashModel staticModels = wrapper.getStaticModels();
- newConfig.setSharedVariable("Static", staticModels);
+ TemplateHashModel rawStaticModels = wrapper.getStaticModels();
+ newConfig.setSharedVariable("Static",
RestrictedStaticModels.fromConfig(rawStaticModels, "freemarker-whitelist"));
try {
- newConfig.setSharedVariable("EntityQuery",
staticModels.get("org.apache.ofbiz.entity.util.EntityQuery"));
+ newConfig.setSharedVariable("EntityQuery",
rawStaticModels.get("org.apache.ofbiz.entity.util.EntityQuery"));
} catch (TemplateModelException e) {
Debug.logError(e, MODULE);
}
diff --git
a/framework/base/src/main/java/org/apache/ofbiz/base/util/template/RestrictedStaticModels.java
b/framework/base/src/main/java/org/apache/ofbiz/base/util/template/RestrictedStaticModels.java
new file mode 100644
index 0000000000..d82532c220
--- /dev/null
+++
b/framework/base/src/main/java/org/apache/ofbiz/base/util/template/RestrictedStaticModels.java
@@ -0,0 +1,178 @@
+/*
+ * 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 org.apache.ofbiz.base.util.template;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.ofbiz.base.util.Debug;
+import org.apache.ofbiz.base.util.UtilProperties;
+
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+
+/**
+ * A {@link TemplateHashModel} that wraps FreeMarker's built-in {@code
StaticModels} and
+ * enforces a whitelist of allowed Java classes and their static members.
+ *
+ * <p>When a FreeMarker template accesses the {@code Static} shared variable
+ * (e.g. {@code Static["org.apache.ofbiz.base.util.UtilMisc"].toMap(...)}),
this
+ * class intercepts the class lookup and the subsequent member access, and
throws
+ * a {@link TemplateModelException} for anything not present in the whitelist.
+ *
+ * <p>The whitelist is loaded from a properties file via
+ * {@link UtilProperties#getProperties(String)}. Each entry has the form:
+ * <pre>
+ * fully.qualified.ClassName = member1,member2,FIELD_NAME
+ * </pre>
+ * An empty value means the class is in the whitelist but none of its members
+ * are accessible. There is no wildcard support — every allowed member must be
+ * listed explicitly.
+ *
+ * <p>If the configuration resource cannot be found, the whitelist is empty and
+ * all {@code Static[...]} access is denied (fail-safe behaviour).
+ *
+ * @see FreeMarkerWorker#makeConfiguration(freemarker.ext.beans.BeansWrapper)
+ */
+public final class RestrictedStaticModels implements TemplateHashModel {
+
+ private static final String MODULE =
RestrictedStaticModels.class.getName();
+
+ private final TemplateHashModel delegate;
+ private final Map<String, Set<String>> whitelist;
+
+ private RestrictedStaticModels(TemplateHashModel delegate, Map<String,
Set<String>> whitelist) {
+ this.delegate = delegate;
+ this.whitelist = whitelist;
+ }
+
+ /**
+ * Builds a {@code RestrictedStaticModels} by loading the whitelist from
the named
+ * properties resource (resolved via {@link
UtilProperties#getProperties(String)}).
+ *
+ * <p>If the resource is not found, the returned instance denies all
access and an
+ * error is logged.
+ *
+ * @param delegate the underlying {@code StaticModels} from the
BeansWrapper
+ * @param configResource the properties resource name (without the {@code
.properties}
+ * extension), e.g. {@code "freemarker-whitelist"}
+ * @return an immutable {@code RestrictedStaticModels} instance
+ */
+ public static RestrictedStaticModels fromConfig(TemplateHashModel
delegate, String configResource) {
+ Properties props = UtilProperties.getProperties(configResource);
+ if (props == null) {
+ Debug.logError("FreeMarker static-member whitelist configuration
not found: ["
+ + configResource + ".properties]. All Static[...] access
will be denied.", MODULE);
+ return new RestrictedStaticModels(delegate,
Collections.emptyMap());
+ }
+
+ Map<String, Set<String>> whitelist = new LinkedHashMap<>();
+ for (String rawClassName : props.stringPropertyNames()) {
+ String className = rawClassName.trim();
+ if (className.isEmpty()) {
+ continue;
+ }
+ String rawValue = props.getProperty(rawClassName, "").trim();
+ if (rawValue.isEmpty()) {
+ whitelist.put(className, Collections.emptySet());
+ } else {
+ Set<String> members = new LinkedHashSet<>();
+ for (String token : rawValue.split(",")) {
+ String member = token.trim();
+ if (!member.isEmpty()) {
+ members.add(member);
+ }
+ }
+ whitelist.put(className, Collections.unmodifiableSet(members));
+ }
+ }
+
+ Debug.logInfo("FreeMarker static-member whitelist loaded from [" +
configResource
+ + ".properties]: " + whitelist.size() + " class(es) allowed.",
MODULE);
+ return new RestrictedStaticModels(delegate,
Collections.unmodifiableMap(whitelist));
+ }
+
+ /**
+ * Returns the {@link TemplateModel} for the requested Java class,
provided it is
+ * in the whitelist.
+ *
+ * <p>The model is always wrapped in a {@link RestrictedStaticModel} that
enforces
+ * member-level access control. An empty allowed-member set means the
class is
+ * whitelisted but every member lookup will be denied.
+ *
+ * @param className the fully-qualified Java class name
+ * @return a (possibly wrapped) {@link TemplateModel} for the class
+ * @throws TemplateModelException if the class is not in the whitelist
+ */
+ @Override
+ public TemplateModel get(String className) throws TemplateModelException {
+ if (!whitelist.containsKey(className)) {
+ throw new TemplateModelException(
+ "FreeMarker static access denied: class [" + className +
"] is not in the"
+ + " whitelist. To allow access, add an entry to
freemarker-whitelist.properties.");
+ }
+ TemplateModel model = delegate.get(className);
+ Set<String> allowedMembers = whitelist.get(className);
+ return new RestrictedStaticModel((TemplateHashModel) model,
allowedMembers, className);
+ }
+
+ @Override
+ public boolean isEmpty() throws TemplateModelException {
+ return whitelist.isEmpty();
+ }
+
+ /**
+ * A {@link TemplateHashModel} wrapper around a single {@code StaticModel}
that
+ * restricts member access to a pre-approved set of names.
+ */
+ private static final class RestrictedStaticModel implements
TemplateHashModel {
+
+ private final TemplateHashModel delegate;
+ private final Set<String> allowedMembers;
+ private final String className;
+
+ private RestrictedStaticModel(TemplateHashModel delegate, Set<String>
allowedMembers,
+ String className) {
+ this.delegate = delegate;
+ this.allowedMembers = allowedMembers;
+ this.className = className;
+ }
+
+ @Override
+ public TemplateModel get(String memberName) throws
TemplateModelException {
+ if (!allowedMembers.contains(memberName)) {
+ throw new TemplateModelException(
+ "FreeMarker static access denied: member [" +
className + "." + memberName
+ + "] is not in the whitelist. To allow access, add it
to"
+ + " freemarker-whitelist.properties.");
+ }
+ return delegate.get(memberName);
+ }
+
+ @Override
+ public boolean isEmpty() throws TemplateModelException {
+ return allowedMembers.isEmpty();
+ }
+ }
+}
diff --git
a/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntitySaxReader.java
b/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntitySaxReader.java
index c428a6a213..b3e357d7a1 100644
---
a/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntitySaxReader.java
+++
b/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntitySaxReader.java
@@ -67,7 +67,6 @@ import freemarker.ext.dom.NodeModel;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
-import freemarker.template.TemplateHashModel;
/**
* SAX XML Parser Content Handler for Entity Engine XML files
@@ -389,8 +388,7 @@ public class EntitySaxReader extends DefaultHandler {
NodeModel nodeModel =
NodeModel.wrap(this.rootNodeForTemplate);
Map<String, Object> context = new HashMap<>();
- TemplateHashModel staticModels =
FreeMarkerWorker.getDefaultOfbizWrapper().getStaticModels();
- context.put("Static", staticModels);
+ context.put("Static",
FreeMarkerWorker.getRestrictedStaticModels());
context.put("doc", nodeModel);
template.process(context, outWriter);
diff --git a/framework/security/config/freemarker-whitelist.properties
b/framework/security/config/freemarker-whitelist.properties
new file mode 100644
index 0000000000..0dfb2fef6e
--- /dev/null
+++ b/framework/security/config/freemarker-whitelist.properties
@@ -0,0 +1,159 @@
+###############################################################################
+# 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.
+###############################################################################
+
+###############################################################################
+# FreeMarker Static Variable Whitelist
+#
+# This file controls which Java classes and static members may be accessed
+# from FreeMarker templates via the "Static" shared variable:
+#
+# Static["fully.qualified.ClassName"].memberName
+#
+# FORMAT
+# ------
+# Each line has the form:
+#
+# fully.qualified.ClassName = member1,member2,FIELD_NAME
+#
+# where members can be static methods OR static fields.
+# An empty value means the class is recognized but no member is accessible.
+#
+# There is intentionally no wildcard support. Every permitted member must
+# be listed explicitly so that the allowed surface area remains auditable.
+#
+# To permit a new class or member:
+# 1. Add (or extend) the entry below.
+# 2. Restart the OFBiz server (the whitelist is loaded once at startup).
+###############################################################################
+
+
+# ===========================================================================
+# Java standard library
+# ===========================================================================
+
+java.awt.ComponentOrientation = getOrientation
+java.lang.Integer = parseInt,valueOf
+java.lang.Long = decode,toHexString
+java.lang.Math = ceil
+# DateFormat.LONG is a static int constant used to obtain a locale-aware
+# date formatter (e.g. DateFormat.getDateInstance(DateFormat.LONG, locale)).
+java.text.DateFormat = LONG,getDateInstance
+# TextStyle.FULL_STANDALONE is an enum constant used for month/day-of-week
+# formatting via java.time.
+java.time.format.TextStyle = FULL_STANDALONE
+# TimeZone.LONG is a static int constant used in TimeZone.getDisplayName().
+java.util.TimeZone = LONG,getDefault
+
+# ===========================================================================
+# OFBiz — Framework
+# ===========================================================================
+
+org.apache.ofbiz.base.component.ComponentConfig = getAllComponents
+org.apache.ofbiz.base.util.Debug = logInfo
+org.apache.ofbiz.base.util.FileUtil = getFile
+org.apache.ofbiz.base.util.KeyStoreUtil = certToString,pemToCert,pemToPkHex
+org.apache.ofbiz.base.util.MessageString = getMessagesForField
+org.apache.ofbiz.base.util.StringUtil = split,toList,wrapString
+org.apache.ofbiz.base.util.UtilDateTime =
availableTimeZones,getDayStart,getTimestamp,\
+ monthBegin,nowDateString,nowTimestamp,timeStampToString,toDateString
+org.apache.ofbiz.base.util.UtilFormatOut =
encodeQueryValue,formatDate,formatDateTime
+org.apache.ofbiz.base.util.UtilHttp =
getFullRequestUrl,getNextUniqueId,isJavaScriptEnabled,\
+ parseMultiFormData,stripViewParamsFromQueryString,urlEncodeArgs
+org.apache.ofbiz.base.util.UtilMisc =
availableLocales,getViewLastIndex,parseLocale,\
+ removeFirst,toList,toMap,toSet
+org.apache.ofbiz.base.util.UtilNumber = formatRuleBasedAmount
+org.apache.ofbiz.base.util.UtilProperties =
getMessage,getPropertyAsInteger,getPropertyValue,\
+ getResourceBundleMap
+org.apache.ofbiz.base.util.UtilValidate = isInteger,isUrlInString
+org.apache.ofbiz.base.util.UtilXml = writeXmlDocument
+org.apache.ofbiz.base.util.string.FlexibleStringExpander =
expandString,getInstance
+
+org.apache.ofbiz.common.CommonWorkers =
getAssociatedStateList,getCountryList,getStateList
+org.apache.ofbiz.common.JsLanguageFilesMappingUtil = getFile
+org.apache.ofbiz.common.geo.GeoWorker = expandGeoGroup,findLatestGeoPoint
+
+org.apache.ofbiz.entity.GenericValue = makeXmlDocument
+org.apache.ofbiz.entity.connection.DBCPConnectionFactory = getDataSourceInfo
+org.apache.ofbiz.entity.model.ModelUtil = dbNameToVarName
+org.apache.ofbiz.entity.util.EntityTypeUtil = hasParentType
+org.apache.ofbiz.entity.util.EntityUtil =
filterByAnd,filterByDate,getFieldListFromEntityList,getFirst
+org.apache.ofbiz.entity.util.EntityUtilProperties = getPropertyValue
+
+org.apache.ofbiz.service.calendar.ExpressionUiHelper =
getCandidateIncludeIds,getDayValueList,\
+
getFirstDayOfWeek,getFrequencyValueList,getLastDayOfWeek,getMonthValueList,getOccurrenceList
+
+org.apache.ofbiz.webapp.WebAppCache = getShared
+org.apache.ofbiz.webapp.WebAppUtil = getControlServletPath
+org.apache.ofbiz.webapp.control.LoginWorker = getAppBarWebInfos
+
+org.apache.ofbiz.widget.model.MenuFactory = getMenuFromLocation
+org.apache.ofbiz.widget.model.ModelWidget = widgetBoundaryCommentsEnabled
+org.apache.ofbiz.widget.model.ThemeFactory = resolveVisualTheme
+
+# ===========================================================================
+# OFBiz — Applications
+# ===========================================================================
+
+org.apache.ofbiz.accounting.invoice.InvoiceWorker =
getInvoiceItemDescription,getInvoiceItemTotal
+org.apache.ofbiz.accounting.payment.PaymentWorker = getPaymentNotApplied
+
+org.apache.ofbiz.content.ContentManagementWorker = getUserName
+org.apache.ofbiz.content.content.ContentWorker =
findAlternateLocaleContent,getContentAssocViewFrom
+
+org.apache.ofbiz.htmlreport.HtmlReport = getInstance
+org.apache.ofbiz.htmlreport.sample.SampleHtmlReport = getReport
+
+org.apache.ofbiz.order.order.OrderReadHelper =
calcItemAdjustment,calcOrderAdjustment,getHelper,\
+
getOrderItemAdjustmentList,getOrderItemAdjustmentsTotal,getOrderItemSubTotal,\
+ getOrderItemSurveyResponse
+org.apache.ofbiz.order.order.OrderReturnServices = getReturnItemInitialCost
+org.apache.ofbiz.order.shoppingcart.product.ProductDisplayWorker =
getRandomCartProductAssoc
+org.apache.ofbiz.order.shoppingcart.shipping.ShippingEstimateWrapper =
getWrapper
+org.apache.ofbiz.order.task.TaskWorker =
getCustomerName,getPrettyStatus,getRoleDescription
+
+org.apache.ofbiz.party.contact.ContactHelper = formatCreditCard
+org.apache.ofbiz.party.contact.ContactMechWorker = isUspsAddress
+org.apache.ofbiz.party.party.PartyHelper = getPartyName
+org.apache.ofbiz.party.party.PartyWorker =
getPartyOtherValues,makeMatchingString
+
+org.apache.ofbiz.product.catalog.CatalogWorker =
getCatalogIdsAvailable,getCatalogName,\
+ getCurrentCatalogId
+org.apache.ofbiz.product.category.CategoryContentWrapper =
getProductCategoryContentAsText,\
+ makeCategoryContentWrapper
+org.apache.ofbiz.product.category.CategoryWorker =
checkTrailItem,getRelatedCategoriesRet,getTrail
+org.apache.ofbiz.product.feature.ParametricSearch = makeCategoryFeatureLists
+org.apache.ofbiz.product.imagemanagement.ImageManagementHelper =
getInternalImageUrl
+org.apache.ofbiz.product.product.ProductContentWrapper =
getProductContentAsText,\
+ makeProductContentWrapper
+org.apache.ofbiz.product.product.ProductEvents = getProductCompareList
+org.apache.ofbiz.product.product.ProductSearchSession =
getSearchOptionsHistoryList,\
+ makeSearchParametersString
+org.apache.ofbiz.product.product.ProductWorker =
findProduct,getGwpAlternativeOptionName,\
+ getParentProduct,isPhysical,isSerialized
+org.apache.ofbiz.product.store.ProductStoreWorker =
checkSurveyResponse,getProductStore,\
+ getProductStoreId,isStoreInventoryAvailable,isStoreInventoryRequired,\
+ isStoreInventoryRequiredAndAvailable
+
+org.apache.ofbiz.shipment.picklist.PickListServices = isBinComplete
+
+# ===========================================================================
+# OFBiz — Plugins
+# ===========================================================================
+org.apache.ofbiz.pricat.AbstractPricatParser = isCommentedExcelExists
+org.apache.ofbiz.pricat.PricatParseExcelHtmlReport = getReport