UIMenuItem: isDisabled() is not checked inside decode()
See original GitHub issue1) Environment
- PrimeFaces version: ALL
- Does it work on the newest released PrimeFaces version? Version? NO
- Does it work on the newest sources in GitHub? NO
- Application server + version: ALL
- Affected browsers: ALL
2) Expected behavior
UIMenuItem
should conform to every other UICommand
component, checking for isDisabled()
before enqueuing an ActionEvent
inside decode()
method.
3) Actual behavior
It is possible to invoke a DISABLED UIMenuItem
using javascript, leading to execution of unintended code on server
4) Steps to reproduce
create a
<h:form id="form">
...
<p:menuitem id="menuitem" disabled="true" action="#{someBean.someAction}" value="disabled menuitem" />
...
</h:form>
and execute PrimeFaces.ab({s:"form:menuitem",p:"form",u:"form",f:"form"})
.
Despite the disabled=“true”, the menuitem is decoded and the action is invoked!
5) Sample XHTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:p="http://primefaces.org/ui">
<h:head>
</h:head>
<h:body>
<h:form id="form">
<p:panel>
<ul>
<li>
execute
<pre>PrimeFaces.ab({s:"form:commandButton2",p:"form",u:"form",f:"form"})</pre>
and the following timestamp will be resetted to "[NOT SET]".
<br />
This means that "#{'#{testMenuitemBean.updateTimestamp}'}" has not been invoked (testMenuitemBean is
@RequestScoped).
</li>
<br />
<br />
<li>
execute
<pre>PrimeFaces.ab({s:"form:menuitem2",p:"form",u:"form",f:"form"})</pre>
and the following timestamp
<h1>will be updated!!</h1>
</li>
</ul>
</p:panel>
<br />
<p:panel id="timestampPanel">timestamp: [#{testMenuitemBean.timestamp}]</p:panel>
<br />
<p:panel>
<p:menuButton id="menuButton1" value="menuButton1">
<p:menuitem id="menuitem1" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
value="enabled menuitem" />
<p:menuitem id="menuitem2" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
update="timestampPanel" value="disabled menuitem" />
</p:menuButton>
<p:commandButton id="commandButton1" action="#{testMenuitemBean.updateTimestamp}" process="@form"
update="timestampPanel" value="enabled commandButton" />
<p:commandButton id="commandButton2" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
update="timestampPanel" value="disabled commandButton" />
</p:panel>
</h:form>
</h:body>
</html>
6) Sample bean
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named
@RequestScoped
public class TestMenuitemBean {
private String timestamp = "NOT SET";
public void updateTimestamp() {
timestamp = String.valueOf(System.currentTimeMillis());
}
public String getTimestamp() {
return timestamp;
}
}
7) Solution
this is the current decode()
of https://github.com/primefaces/primefaces/blob/master/src/main/java/org/primefaces/component/menuitem/UIMenuItem.java
@Override
public void decode(FacesContext facesContext) {
Map<String, String> params = facesContext.getExternalContext().getRequestParameterMap();
String clientId = getClientId(facesContext);
if (params.containsKey(clientId)) {
queueEvent(new ActionEvent(this));
}
ComponentUtils.decodeBehaviors(facesContext, this);
}
just add:
@Override
public void decode(FacesContext facesContext) {
if(isDisabled()) { // <------------ this check
return;
}
Map<String, String> params = facesContext.getExternalContext().getRequestParameterMap();
String clientId = getClientId(facesContext);
if (params.containsKey(clientId)) {
queueEvent(new ActionEvent(this));
}
ComponentUtils.decodeBehaviors(facesContext, this);
}
However, since UIMenuItem
has to be used inside another “menu-enabled” component (like TieredMenu
, SlideMenu
, MenuButton
, …), it would be better to check also if the “menu-enabled” ancestor is disabled.
As of now, I didn’t found a simpler way to reach that ancestor, so I’m doing:
@Override
public void decode(FacesContext facesContext)
{
if(isDisabled()) {
return;
}
UIComponent ancestor = getParent();
while(ancestor instanceof UICommand || ancestor instanceof AbstractMenu || ancestor instanceof Submenu) {
if(!ancestor.isRendered() || Boolean.valueOf(String.valueOf(ancestor.getAttributes().get("disabled")))) {
return;
}
ancestor = ancestor.getParent();
}
super.decode(facesContext);
}
Thank you and good work!
Issue Analytics
- State:
- Created 4 years ago
- Comments:12 (7 by maintainers)
@Rapster
You are right, I focused on containers of just menuitems, but we should look also for generic containers that can have
disabled
and, eventually,readonly
attributes.Yes, I know. See the final code below for a better compromise.
The definition does not specify the layer on purpose. It’s necessary and sufficient condition that the same check is performed on server-side, any layer. In this respect, the developer should be relieved of the burden and must be able to trust the framework as a delegate for server side access control rules enforcement, which is exactly the duty of JSF ViewState and Component-Tree. In other words, you shouldn’t be required to check twice by yourself (it’s the JSF killer-feature), otherwise it’s not JSF anymore, but another spring-whatever-DIY-like framework. The framework is doing a great job at this, just need to fill some small hole.
Checking the component hierarchy in
decode()
for everyUICommand
is overwhelming: it adds O(n * h) to theAPPLY_REQUEST_VALUES
phase. Nevertheless, using a globalActionListener
does not have a relevant impact on performance, since it’s called only for the component to be invoked, and will just traverse the tree bottom-up, using O(h) complexity - which is negligible. In the end, is it worth it? Of course. Is there a better solution? There is, but it requires more work (see below).I agree.
In this specific case, the big problem is that
UIMenuItem.decode()
does not check forisDisabled()
in contrast with all othersUICommand
components. More generally, allowing execution of aUICommand
that is a descendant of adisabled
component is not a theoretical violation, but it’s clearly an unwanted behavior, since it’s impossible to achive that behavior with normal user actions on the GUI, but it can be achieved with specially crafted JS. This seems to me the exact definition of vulnerability.This is the last version of the test page:
And the following it’s the rendering
As you can see, the salmon colored represent the critical bypass, while the orange colored represent the unwanted behavior (the GUI does not allow to “click” those menuitems, but JS code can execute them anyway). Unrendered components are safe.
To prevent the critical bypass while having no performance impact, we should update the
UIMenuItem.decode()
adding just theisDisabled()
check:To prevent the unwanted behavior while having no performance impact (we got a little speedup instead), we should add the
AbstractMenu.processDecodes()
adding just theisDisabled()
check:The downsides of this approach are:
UICommands
(i.e.Tab
component is another one that’s impacted)Yep open another issue. We will close this one with just the simply fix of #1 checking for disabled in UIMenuItem.