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

ashishvijaywargiya 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 555f76f7e3 Fixed: arbitrary service execution via Content.serviceName 
in ContentWorker (#1788)
555f76f7e3 is described below

commit 555f76f7e3679d03f4813ba90edebf57a5fd27a3
Author: Ashish Vijaywargiya <[email protected]>
AuthorDate: Thu Aug 27 18:10:08 2026 +0530

    Fixed: arbitrary service execution via Content.serviceName in ContentWorker 
(#1788)
    
    renderContentAsText resolved and ran whatever service name was stored in
    Content.serviceName, a plain non-primary-key field writable by any
    CONTENTMGR_CREATE/UPDATE account through createContent/updateContent,
    using the live HTTP request parameters as the service's input. Some
    content-rendering routes require no authentication, so an armed row
    could be triggered anonymously.
    
    - ContentWorker no longer reads Content.serviceName; the customMethodId
    path now requires the referenced CustomMethod be typed CONTENT_RENDER
    - Locked serviceName and customMethodId out of mass assignment on
    createContent, updateContent, createProductContent and
    updateProductContent
    - Removed the now-dead serviceName field from the Catalog Manager
    external content form
    - Added CONTENT_RENDER as a seed CustomMethodType
    - Added regression coverage for the new dispatch behavior
    
    Thank you Krishna Uprit for your help.
---
 .../content/servicedef/services_content.xml        |  8 +++
 .../ofbiz/content/content/ContentWorker.java       | 16 ++++-
 .../ofbiz/content/content/ContentTests.groovy      | 79 ++++++++++++++++++++++
 applications/product/servicedef/services.xml       |  8 +++
 .../product/widget/catalog/ProductForms.xml        |  1 -
 framework/common/data/CommonTypeData.xml           |  3 +
 6 files changed, 112 insertions(+), 3 deletions(-)

diff --git a/applications/content/servicedef/services_content.xml 
b/applications/content/servicedef/services_content.xml
index 4adcb78963..5857a4476d 100644
--- a/applications/content/servicedef/services_content.xml
+++ b/applications/content/servicedef/services_content.xml
@@ -47,6 +47,10 @@
         <override name="statusId" default-value="CTNT_IN_PROGRESS"/>
         <override name="contentName" allow-html="safe"/>
         <override name="description" allow-html="safe"/>
+        <!-- serviceName/customMethodId choose what code renderContentAsText 
runs against the live request
+             parameters; neither is set through this generic mass-assignment 
path (see ContentWorker.java) -->
+        <override name="serviceName" mode="OUT"/>
+        <override name="customMethodId" mode="OUT"/>
     </service>
 
     <service name="createTextAndUploadedContent" engine="groovy" auth="true"
@@ -140,6 +144,10 @@
         <!-- end of deprecated fields -->
         <override name="contentName" allow-html="safe"/>
         <override name="description" allow-html="safe"/>
+        <!-- serviceName/customMethodId choose what code renderContentAsText 
runs against the live request
+             parameters; neither is set through this generic mass-assignment 
path (see ContentWorker.java) -->
+        <override name="serviceName" mode="OUT"/>
+        <override name="customMethodId" mode="OUT"/>
     </service>
 
     <service name="updateTextContent" engine="group" auth="true">
diff --git 
a/applications/content/src/main/java/org/apache/ofbiz/content/content/ContentWorker.java
 
b/applications/content/src/main/java/org/apache/ofbiz/content/content/ContentWorker.java
index f7895cb53a..914f51ba61 100644
--- 
a/applications/content/src/main/java/org/apache/ofbiz/content/content/ContentWorker.java
+++ 
b/applications/content/src/main/java/org/apache/ofbiz/content/content/ContentWorker.java
@@ -197,10 +197,22 @@ public class ContentWorker implements 
org.apache.ofbiz.widget.content.ContentWor
         // if the content has a service attached run the service
 
         Delegator delegator = dispatcher.getDelegator();
-        String serviceName = content.getString("serviceName"); //Kept for 
backward compatibility
+        // NOTE: Content.serviceName is a legacy, client-writable field 
(createContent/updateContent both
+        // accept it as a plain non-PK attribute) that used to be resolved and 
run here directly; that let
+        // anyone able to write a Content row choose an arbitrary service to 
run with the live HTTP request
+        // parameters as its input. It is no longer honored. Only a 
CustomMethod explicitly typed for
+        // content rendering may be run, so arming a row requires a 
CustomMethod that was deliberately set
+        // up for this purpose, not just a string dropped into the Content row 
itself.
+        String serviceName = null;
         GenericValue custMethod = null;
         if (UtilValidate.isNotEmpty(content.getString("customMethodId"))) {
-            custMethod = 
EntityQuery.use(delegator).from("CustomMethod").where("customMethodId", 
content.get("customMethodId")).cache().queryOne();
+            custMethod = EntityQuery.use(delegator).from("CustomMethod")
+                    .where("customMethodId", content.get("customMethodId"), 
"customMethodTypeId", "CONTENT_RENDER")
+                    .cache().queryOne();
+            if (custMethod == null) {
+                throw new GeneralException("customMethodId [" + 
content.get("customMethodId")
+                        + "] on content [" + content.get("contentId") + "] is 
not a content rendering method");
+            }
         }
         if (custMethod != null) serviceName = 
custMethod.getString("customMethodName");
         if (UtilValidate.isNotEmpty(serviceName)) {
diff --git 
a/applications/content/src/test/groovy/org/apache/ofbiz/content/content/ContentTests.groovy
 
b/applications/content/src/test/groovy/org/apache/ofbiz/content/content/ContentTests.groovy
index 801f0fc75d..0296d5f38d 100644
--- 
a/applications/content/src/test/groovy/org/apache/ofbiz/content/content/ContentTests.groovy
+++ 
b/applications/content/src/test/groovy/org/apache/ofbiz/content/content/ContentTests.groovy
@@ -18,6 +18,9 @@
  
*******************************************************************************/
 package org.apache.ofbiz.content.content
 
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+import org.apache.ofbiz.base.util.GeneralException
 import org.apache.ofbiz.base.util.UtilDateTime
 import org.apache.ofbiz.entity.GenericValue
 import org.apache.ofbiz.service.ServiceUtil
@@ -221,4 +224,80 @@ class ContentTests implements JupiterTestHelper {
         assert serviceResult.view
     }
 
+    // Regression coverage for the fix to renderContentAsText's service 
dispatch: Content.serviceName is a
+    // legacy, client-writable field (createContent/updateContent both let a 
plain content editor set it) that
+    // used to be looked up and run directly with the live HTTP request 
parameters as its input. Only a
+    // CustomMethod explicitly typed CONTENT_RENDER may be run there now.
+
+    // These three build their own Content/CustomMethod fixture rows inline 
(rather than relying on the
+    // testdef data-load test-case) since that data-load and this Jupiter 
suite are proven, in this class,
+    // not to share read-your-writes visibility of a row committed in between 
the two.
+
+    @Test
+    @Order(9)
+    void testRenderContentAsTextIgnoresServiceName() {
+        String contentId = testParams.contentId ?: 'TEST_CNT_SVCNAME'
+        String dataResourceId = testParams.dataResourceId ?: 'TEST_DR_SVCNAME'
+        delegator.create('DataResource', [dataResourceId: dataResourceId, 
dataResourceTypeId: 'ELECTRONIC_TEXT'])
+        delegator.create('ElectronicText', [dataResourceId: dataResourceId, 
textData: 'Test text for service name ignore check'])
+        delegator.create('Content', [contentId: contentId, contentTypeId: 
'TEST_CONTENT_TYPE',
+                dataResourceId: dataResourceId, serviceName: 
'aServiceThatDefinitelyDoesNotExist'])
+        GenericValue content = from('Content').where('contentId', 
contentId).queryOne()
+        assert content
+        assert content.serviceName
+
+        StringWriter out = new StringWriter()
+        Map<String, Object> templateContext = [userLogin: userLogin, 
requestParameters: [:]]
+        // must render the underlying data straight through, not attempt to 
resolve/run the bogus service name
+        ContentWorker.renderContentAsText(dispatcher, content, out, 
templateContext, Locale.US, 'text/plain', false, null)
+        assert out.toString().contains('Test text for service name ignore 
check')
+    }
+
+    @Test
+    @Order(10)
+    void testRenderContentAsTextRunsTypedCustomMethod() {
+        String contentId = testParams.contentId ?: 'TEST_CNT_CM_RENDER'
+        String customMethodId = testParams.customMethodId ?: 'TEST_CM_RENDER'
+        String dataResourceId = testParams.dataResourceId ?: 
'TEST_DR_CM_RENDER'
+        delegator.create('DataResource', [dataResourceId: dataResourceId, 
dataResourceTypeId: 'ELECTRONIC_TEXT'])
+        delegator.create('ElectronicText', [dataResourceId: dataResourceId, 
textData: 'Test text for typed custom method check'])
+        // CONTENT_RENDER is shipped seed data 
(framework/common/data/CommonTypeData.xml)
+        delegator.create('CustomMethod', [customMethodId: customMethodId, 
customMethodTypeId: 'CONTENT_RENDER',
+                customMethodName: 'getDataResource'])
+        delegator.create('Content', [contentId: contentId, contentTypeId: 
'TEST_CONTENT_TYPE',
+                dataResourceId: dataResourceId, customMethodId: 
customMethodId])
+        GenericValue content = from('Content').where('contentId', 
contentId).queryOne()
+        assert content
+
+        StringWriter out = new StringWriter()
+        Map<String, Object> templateContext = [userLogin: userLogin,
+                                                 requestParameters: 
[dataResourceId: dataResourceId]]
+        ContentWorker.renderContentAsText(dispatcher, content, out, 
templateContext, Locale.US, 'text/plain', false, null)
+        // the invoked service's OUT parameters were merged into 
templateContext
+        assert templateContext.resultData
+        assert out.toString().contains('Test text for typed custom method 
check')
+    }
+
+    @Test
+    @Order(11)
+    void testRenderContentAsTextRejectsCustomMethodOfWrongType() {
+        String contentId = testParams.contentId ?: 'TEST_CNT_CM_WRONG'
+        String customMethodId = testParams.customMethodId ?: 
'TEST_CM_WRONG_TYPE'
+        // TELECOM_GATEWAY is shipped seed data 
(framework/common/data/CommonTypeData.xml), unrelated to
+        // content rendering - any type other than CONTENT_RENDER proves the 
point
+        delegator.create('CustomMethod', [customMethodId: customMethodId, 
customMethodTypeId: 'TELECOM_GATEWAY',
+                customMethodName: 'getDataResource'])
+        delegator.create('Content', [contentId: contentId, contentTypeId: 
'TEST_CONTENT_TYPE',
+                dataResourceId: 'TEST_CONTENT_TEXT1', customMethodId: 
customMethodId])
+        GenericValue content = from('Content').where('contentId', 
contentId).queryOne()
+        assert content
+
+        StringWriter out = new StringWriter()
+        Map<String, Object> templateContext = [userLogin: userLogin, 
requestParameters: [:]]
+        GeneralException exception = assertThrows(GeneralException) {
+            ContentWorker.renderContentAsText(dispatcher, content, out, 
templateContext, Locale.US, 'text/plain', false, null)
+        }
+        assert exception.message.contains('not a content rendering method')
+    }
+
 }
diff --git a/applications/product/servicedef/services.xml 
b/applications/product/servicedef/services.xml
index 2e429a07c4..20e710dc88 100644
--- a/applications/product/servicedef/services.xml
+++ b/applications/product/servicedef/services.xml
@@ -366,6 +366,10 @@ under the License.
         <auto-attributes include="nonpk" mode="IN" optional="true"/>
         <auto-attributes entity-name="Content" include="nonpk" mode="IN" 
optional="true"/>
         <override name="fromDate" default-value="${date:nowTimestamp()}"/>
+        <!-- serviceName/customMethodId choose what code renderContentAsText 
runs against the live request
+             parameters; neither is set through this generic mass-assignment 
path (see ContentWorker.java) -->
+        <override name="serviceName" mode="OUT"/>
+        <override name="customMethodId" mode="OUT"/>
     </service>
     <service name="updateProductContent" default-entity-name="ProductContent" 
engine="groovy"
         
location="component://product/src/main/groovy/org/apache/ofbiz/product/product/product/ProductContentServicesScript.groovy"
 invoke="updateProductContent" auth="true">
@@ -373,6 +377,10 @@ under the License.
         <auto-attributes include="pk" mode="IN" optional="false"/>
         <auto-attributes include="nonpk" mode="IN" optional="true"/>
         <auto-attributes entity-name="Content" include="nonpk" mode="IN" 
optional="true"/>
+        <!-- serviceName/customMethodId choose what code renderContentAsText 
runs against the live request
+             parameters; neither is set through this generic mass-assignment 
path (see ContentWorker.java) -->
+        <override name="serviceName" mode="OUT"/>
+        <override name="customMethodId" mode="OUT"/>
     </service>
     <service name="removeProductContent" default-entity-name="ProductContent" 
engine="entity-auto" invoke="delete" auth="true">
         <description>Remove Content From Product</description>
diff --git a/applications/product/widget/catalog/ProductForms.xml 
b/applications/product/widget/catalog/ProductForms.xml
index 22b906158a..40c1f9af7b 100644
--- a/applications/product/widget/catalog/ProductForms.xml
+++ b/applications/product/widget/catalog/ProductForms.xml
@@ -773,7 +773,6 @@ under the License.
         <field name="description" 
title="${uiLabelMap.ProductProductDescription}" map-name="content"><text 
size="40"/></field>
         <field name="fromDate" title="${uiLabelMap.CommonFromDate}" ></field>
         <field name="thruDate" title="${uiLabelMap.CommonThruDate}"></field>
-        <field name="serviceName" title="${uiLabelMap.ProductServiceName}" 
map-name="content"><text size="40"/></field>
         <field use-when="contentId == null" name="contentId" 
title="${uiLabelMap.ProductContentId}" 
tooltip="${uiLabelMap.ProductOptional}"><text maxlength="20"/></field>
         <field use-when="contentId != null" name="contentId" 
title="${uiLabelMap.ProductContentId}" 
tooltip="${uiLabelMap.ProductNotModificationRecrationProductContentAssociation}"
 map-name="productContentData" ><display/></field>
         <field name="productId"><hidden/></field>
diff --git a/framework/common/data/CommonTypeData.xml 
b/framework/common/data/CommonTypeData.xml
index 70c32c91b3..53c675eed6 100644
--- a/framework/common/data/CommonTypeData.xml
+++ b/framework/common/data/CommonTypeData.xml
@@ -152,4 +152,7 @@ under the License.
     <TelecomMethodType telecomMethodTypeId="SMS" description="Short Messaging 
Service Method"/>
     <TelecomMethodType telecomMethodTypeId="WHATSAPP" description="WhatsApp 
Messaging Service Method"/>
     <CustomMethodType customMethodTypeId="TELECOM_GATEWAY" 
description="Telecom Gateway Custom Method"/>
+
+    <!-- Content Rendering Custom Method -->
+    <CustomMethodType customMethodTypeId="CONTENT_RENDER" description="Content 
Rendering Method"/>
 </entity-engine-xml>

Reply via email to