This is an automated email from the ASF dual-hosted git repository.

jacopoc pushed a commit to branch release24.09
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git

commit 6adadde4fbae6613cd41c1680761cf1f12a7365e
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 108eb98c3f..1721029aeb 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

Reply via email to