CVE-2026-73577: Apache OFBiz Path-Suffix View Authorization Bypass to RCE
TL;DR
- An unauthenticated attacker could run commands as the
ofbizoperating-system account through a public Catalog route. - OFBiz checked authentication against the first component of a multi-part
/control/path, then rendered the view named by the remaining suffix. - A direct request to
showPortalPagereturned HTTP 401. The publicListLocalesroute rendered the same view as a suffix with HTTP 200. - The portal reached a Party widget whose menu location came from an anonymous request parameter. A widget document held open in OFBiz's multipart upload spool supplied the executable resource.
- CVE-2026-73577 affects Apache OFBiz before 24.09.08 and is fixed in 24.09.08, which checks whether an authenticated request map hands out the suffix-selected view before rendering it anonymously.
Summary
- CVE: CVE-2026-73577
- Product: Apache OFBiz (Java ERP suite)
- Vulnerability: Unauthenticated View Authorization Bypass to Remote Code Execution
- Affected Versions: < 24.09.08
- Fixed In: 24.09.08
- Severity: Important (per Apache)
- CVSS Severity: 9.8 (critical, reporter-assessed)
- Required Privilege: None
- Advisory: Apache OFBiz CVE Announcements
- Reported: August 2, 2026
- NVD Published: November 5, 2026
Introduction
OFBiz can map one path under /control/ to two route identities. The first path component selects the request map, while the rest can replace its response view.
Most of the time that distinction is invisible, because a request map and the view it returns are written together and share an author's intent. Their security settings are independent, though. Request maps and view maps carry their own auth flags, and plenty of views are marked public on the understanding that only an authenticated route ever reaches them. Path-suffix substitution gives them a second way in.
Root Cause Analysis
The excerpts below are from the tested release24.09 snapshot at commit b5ca713e.
One Path with Two Identities
The split happens in RequestHandler.resolveURI().
// framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
String path = req.getPathInfo();
String requestUri = getRequestUri(path); // [1] Request-map name.
String overrideViewUri = getOverrideViewUri(path); // [2] Suffix view name.
if (requestMapMap.containsKey(requestUri)
// Ensure that overridden view exists.
&& (overrideViewUri == null || viewMapMap.containsKey(overrideViewUri)
|| ("SOAPService".equals(requestUri) && "wsdl".equalsIgnoreCase(req.getQueryString())))) {
rmaps = requestMapMap.get(requestUri); // [3] Request-map lookup.
req.setAttribute("overriddenView", overrideViewUri); // [4] Store suffix view.
// ... else branches omitted; they fall back to the default request.
In RequestHandler.resolveURI(), one path becomes two names at [1] and [2], so /catalog/control/ListLocales/showPortalPage is the ListLocales request carrying showPortalPage as its view. Dispatch keeps the request map at [3] and parks the view name at [4]. Both names must exist. The selected view can belong to a different request map from the one supplying the security policy.
Authentication Checks the Public Half
Catalog imports the common and portal controllers, and the common one exposes ListLocales with no login.
<!-- framework/common/webcommon/WEB-INF/common-controller.xml -->
<request-map uri="ListLocales"><security https="true" auth="false"/><response name="success" type="view" value="ListLocales" save-last-view="true"/></request-map>
The portal controller expects showPortalPage to be reached through an authenticated request, and marks the matching view public.
<!-- framework/common/webcommon/WEB-INF/portal-controller.xml -->
<request-map uri="showPortalPage">
<security https="true" auth="true"/>
<response name="success" type="view" value="showPortalPage" save-home-view="true"/>
</request-map>
<!-- framework/common/webcommon/WEB-INF/portal-controller.xml -->
<view-map name="showPortalPage" type="screen" page="component://common/widget/PortalPageScreens.xml#showPortalPage" auth="false"/>
The authenticated request map and its public view map leave the auth="false" view relying on its request map for protection. Another request map can select it without that request map's login check.
The login check runs against the chosen request map, which here is ListLocales. The substitution happens afterwards, once the response is already a success.
// framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java
// [5] Suffix selects view.
String viewName = (UtilValidate.isNotEmpty(overrideViewUri) && (eventReturn == null || "success".equals(eventReturn)))
? overrideViewUri : nextRequestResponse.getValue();
renderView(viewName, requestMap.isSecurityExternalView(), request, response, saveName);
In RequestHandler, the assignment at [5] never revisits the request map that owns the chosen view, and the renderer then checks the view's own auth flag, which is false. The authenticated showPortalPage request map is never consulted.
A Seeded Portal Reaches a Cross-Application Widget
Seed data creates the SFA_MAIN page for the anonymous _NA_ owner and hangs a Party portlet on it.
<!-- applications/marketing/data/SfaPortletData.xml -->
<PortalPage portalPageId="SFA_MAIN" sequenceNum="0" portalPageName="Main" description="The SFA main page" ownerUserLoginId="_NA_"/>
<PortalPageColumn portalPageId="SFA_MAIN" columnSeqId="00001"/>
<PortalPagePortlet portalPageId="SFA_MAIN" portalPortletId="MyCommunications" portletSeqId="00001" columnSeqId="00001" sequenceNum="0"/>
<PortalPagePortlet portalPageId="SFA_MAIN" portalPortletId="Calendar" portletSeqId="00001" columnSeqId="00001" sequenceNum="1"/>
PortalPageWorker represents an anonymous user as _NA_, matching the page owner. The body renderer then loads each portlet's recorded screen name and location. Nothing ties that metadata to the current web application, so Catalog renders an SFA page and its Party portlet.
That Party screen takes its menu location from a parameter.
<!-- applications/party/widget/partymgr/CommunicationEventScreens.xml -->
<screenlet title="${uiLabelMap.PartyCommunicationsOfParty}: ${partyName.firstName} ${partyName.middleName} ${partyName.lastName} ${partyName.groupName} [${partyId}] "
navigation-menu-name="CommunicationsMenu" navigation-form-name="ListPartyCommEvents" name="myComms">
<include-menu name="CommunicationsMenu" location="${parameters.communicationMenuLocation}"/>
SFA supplies that setting from its own configuration. Catalog does not define it, and parameters is a merge of several scopes with request parameters at the bottom.
// framework/base/src/main/java/org/apache/ofbiz/base/util/UtilHttp.java
public static Map<String, Object> getCombinedMap(HttpServletRequest request, Set<? extends String> namesToSkip) {
Map<String, Object> combinedMap = new HashMap<>();
// [6] Request parameter kept.
combinedMap.putAll(getParameterMap(request)); // parameters override nothing
combinedMap.putAll(getServletContextMap(request, namesToSkip)); // bottom level application attributes
combinedMap.putAll(getSessionMap(request, namesToSkip)); // session overrides application
combinedMap.putAll(getAttributeMap(request)); // attributes trump them all
return combinedMap;
}
UtilHttp.getCombinedMap() puts request parameters into the map first at [6], so higher-precedence scopes can overwrite them. Under Catalog nothing supplies the same name, so the anonymous form field reaches the Party widget intact.
Keeping an Uploaded Widget Open
OFBiz's multipart upload spool supplies the file that the menu loader needs. Multipart handling runs before controller dispatch, where UtilHttp builds Commons FileUpload's disk-backed parser using the shipped settings.
# framework/common/config/general.properties
# -- HTTP upload settings
# -- Size threshold (almost 1MB)
# [7] Disk spool threshold.
http.upload.max.sizethreshold=1024000
# -- directory used to temporarily store files that are larger than the configured size threshold
http.upload.tmprepository=runtime/tmp
# -- max size in bytes (-1 for unlimited)
http.upload.max.size=-1
The shipped upload settings move a staged document out of memory and into an open temporary file once it crosses the threshold at [7]. Send a complete widget menu followed by harmless XML whitespace, declare one more byte than you send, and FileUpload waits for the rest with its output file still open. On Linux another request thread can read that same file through /proc/thread-self/fd/N.
Loading the Descriptor as a Menu
MenuFactory.getMenuFromLocation() resolves and parses whatever it is given.
// framework/widget/src/main/java/org/apache/ofbiz/widget/model/MenuFactory.java
URL menuFileUrl = FlexibleLocation.resolveLocation(resourceName); // [8] Resolve menu URL.
if (menuFileUrl == null || UtilValidate.isUrlInStringAndDoesNotStartByComponentProtocol(menuFileUrl.toString())) {
throw new IllegalArgumentException("Could not resolve location to URL: " + resourceName);
}
Document menuFileDoc = UtilXml.readXmlDocument(menuFileUrl, true, true); // [9] Parse menu XML.
modelMenuMap = readMenuDocument(menuFileDoc, resourceName, visualTheme);
MenuFactory.getMenuFromLocation() turns the string into a URL at [8] and XML at [9]. The guard between them rejects non-component strings containing ://. The single-slash URL file:/proc/thread-self/fd/402 passes.
A widget menu can carry a ${groovy:...} expression in an item title, and FlexibleStringExpander compiles and evaluates it while rendering.
// framework/base/src/main/java/org/apache/ofbiz/base/util/string/FlexibleStringExpander.java
this.script = new String(this.getChars(), this.parseStart, this.parseLength);
this.parsedScript = ScriptUtil.parseScript(this.language, this.script); // [10] Compile Groovy.
// ... constructor closing and method setup omitted; they do not affect evaluation.
Object obj = ScriptUtil.evaluate(this.language, this.script, this.parsedScript, contextCopy); // [11] Execute Groovy.
The widget is compiled at [10] and evaluated at [11]. A regular-expression denylist over the script's source text guards that, and Groovy's dynamic dispatch bypasses it.
Impact
An unauthenticated client can run commands as the operating-system account that owns the OFBiz process, including reading or changing the database credentials in OFBiz's configuration.
The authorisation bypass can render a protected view anonymously without Linux descriptors, multipart staging or a shell. What another suffix-selected view exposes depends on that view's own checks.
Exploitation
Preconditions
- Apache OFBiz on the affected range, with Catalog reachable. The default component loader includes Product, Party and Marketing, so the whole route is present in a stock install.
- The
SFA_MAINportal page and itsMyCommunicationsportlet, which theseedandseed-initialreaders load by default. Deleting those rows removes this RCE tail. The underlying authorisation bypass remains. - A Linux-style descriptor namespace, writable multipart storage and a shell for the RCE tail.
- Ingress that streams the multipart body rather than buffering it, with both requests reaching the same OFBiz JVM.
The shipped configuration selects NoCsrfDefenseStrategy. With the stronger strategy enabled, the client takes an anonymous AJAX token from the login response and carries on.
PoC
The PoC holds a multipart upload open, searches the descriptor range and prints the command output.
python3 cve_2026_73577.py https://target.example:8443 -c 'id' --fd-min 200 --fd-max 800
___ _____ ____ ___ _____
/ _ \| ___| __ )_ _|__ /
| | | | |_ | _ \| | / /
| |_| | _| | |_) | | / /_
\___/|_| |____/___/____|
path-suffix view bypass to RCE · CVE-2026-73577
discovered by CryptoCat · cryptocat.me
[!] command execution is at-least-once; use an idempotent command
[*] Bootstrapping an anonymous Catalog session through /catalog/control/main?visualThemeId=COMMON
[*] Bootstrap response: HTTP 401
[+] Anonymous session cookie obtained
[+] Anonymous AJAX CSRF token obtained (12 characters)
[*] Intended /catalog/control/showPortalPage route: HTTP 401
[*] Staging 1,200,000-byte menu through /catalog/control/main
[+] Path-suffix portal chain loaded the staged menu through /proc/thread-self/fd/393
[+] Carrier response was HTTP 200 and the intended route was protected
[+] Remote command exit status: 0
[+] command output
------------------------------------------------------------
uid=1000(ofbiz) gid=1000(ofbiz) groups=1000(ofbiz)
That run is against the release24.09.07 tag. The later branch snapshot behaved the same. A timeout or malformed frame leaves the execution status unknown. Use an idempotent command and do not retry anything that cannot safely run twice.
Demo
The same request in a browser renders the portal with no session. The staged menu runs where the Party communications widget would be, so the id output lands in the page itself, served over the public ListLocales suffix.

Patch Diffing
The fix is e469a3956a, committed on 13 August. It adds one lookup, which asks whether the suffix-selected view is a view that some authenticated request map hands out. The commit is an ancestor of the final 24.09.08 release tag.
// framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java (overrideViewRequiresAuth)
+static boolean overrideViewRequiresAuth(ControllerConfig ccfg, String overrideViewUri) {
+ if (overrideViewUri == null) {
+ return false;
+ }
+ for (List<RequestMap> maps : ccfg.getRequestMapMultiMap().values()) {
+ for (RequestMap requestMap : maps) {
+ if (!requestMap.isSecurityAuth()) {
+ continue;
+ }
+ for (ConfigXMLReader.RequestResponse response : requestMap.getRequestResponseMap().values()) {
+ if ("view".equals(response.getType()) && overrideViewUri.equals(response.getValue())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
The search walks every request map in the controller, skips the ones that do not require a login, and reads each one's declared view responses. If the suffix names a view that any authenticated request map returns, the answer is yes.
That answer is folded into the security check that previously consulted the matched request map alone.
// framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/RequestHandler.java (doRequest)
}
// Perform security check.
-if (requestMap.isSecurityAuth()) {
+// A view-suffix override (see resolveURI/getOverrideViewUri) must not let an anonymous
+// request map render a view that is otherwise only ever handed out by an authenticated
+// request map: treat that case as if this request map itself required auth.
+if (requestMap.isSecurityAuth() || overrideViewRequiresAuth(ccfg, overrideViewUri)) {
// Invoke the security handler
// catch exceptions and throw RequestHandlerException if failed.
if (Debug.verboseOn()) {
The effective view now carries the strictest policy of any request map that declares it, so a public carrier cannot lend its policy to a protected view. The check searches the whole controller and covers every view with this configuration.
The commit also closes the specific instance directly.
// framework/common/webcommon/WEB-INF/portal-controller.xml (view-map)
-<view-map name="showPortalPage" type="screen" page="component://common/widget/PortalPageScreens.xml#showPortalPage" auth="false"/>
+<view-map name="showPortalPage" type="screen" page="component://common/widget/PortalPageScreens.xml#showPortalPage" auth="true"/>
The portal view now requires authentication too. Regression tests ship alongside both changes in RequestHandlerTests.java.
Remediation
Upgrade to Apache OFBiz 24.09.08 or later.
On older builds, grep your controllers for views declared auth="false" that an authenticated request map also hands out. Blocking multi-segment paths under /control/ at the edge would remove the carrier. OFBiz deliberately uses suffix-selected views, so this mitigation can break legitimate routing.
Disclosure Timeline
- August 2, 2026: Reported to the Apache OFBiz security team.
- August 13, 2026: Apache assigned CVE-2026-73577, shared the draft advisory and committed the fix with regression tests.
- November 3, 2026: Apache announced OFBiz 24.09.08, the first released version containing the fix.
- November 4, 2026: Apache publicly announced CVE-2026-73577 in its consolidated OFBiz CVE notice.
- November 5, 2026: CVE.org and NVD published CVE-2026-73577.
Conclusion
OFBiz applied the suffix after deciding authentication, so the selected view never received the policy of the request map that owned it. The patch checks that relationship before serving the view.