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

