question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

UIMenuItem: isDisabled() is not checked inside decode()

See original GitHub issue

1) 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:closed
  • Created 4 years ago
  • Comments:12 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
mmariotticommented, Feb 15, 2020

@Rapster

I think you’re wrong, most of our component can p:panel, p:dataTable, p:outputPanel etc.

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.

First, if we were to apply this fix (which looks correct) it’s a performance killer. Depending the number of components in your tree and commands, this fix will have side effect

Yes, I know. See the final code below for a better compromise.

Second, in your sample let’s say you disabled this button because the user is not allowed to. Even using your hack, the method called (service method probably) should be secured and throw an exception. For more details, see https://owasp-aasvs.readthedocs.io/en/latest/requirement-4.9.html

as ASVS rule mentionned, security check should be done on server side (I think they mean business layer in our case)

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.

Finally, I think this fix could prevent some hacks yes and even though your impl SecurityActionListener looks better. I have one concern: performance. Is it worth to slow down performance for a use case that will happen rarely?

Checking the component hierarchy in decode() for every UICommand is overwhelming: it adds O(n * h) to the APPLY_REQUEST_VALUES phase. Nevertheless, using a global ActionListener 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).

Last thought, adding SecurityActionListener could be optional though it’s up to the user to enable it or not

I agree.

IMO I think it’s a workaround to the real problem

In this specific case, the big problem is that UIMenuItem.decode() does not check for isDisabled() in contrast with all others UICommand components. More generally, allowing execution of a UICommand that is a descendant of a disabled 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:

<h:form id="form">
    <p:panelGrid columns="3" style="width: 100%">
        <f:facet name="header">
            <p:outputPanel id="timestampPanel">timestamp: [#{testMenuitemBean.timestamp}]</p:outputPanel>
        </f:facet>

        <h:panelGroup>
            <p:commandButton id="b1" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                value="b1" />

            <div>
                <p:commandButton type="button" value="hack b1" onclick="PrimeFaces.ab({s:'form:b1',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>

        <h:panelGroup>
            <p:commandButton id="b2" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                update="timestampPanel" value="b2" />

            <div>
                <p:commandButton type="button" value="hack b2" onclick="PrimeFaces.ab({s:'form:b2',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:commandButton id="b3" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                update="timestampPanel" value="b3" />

            <div>
                <p:commandButton type="button" value="hack b3" onclick="PrimeFaces.ab({s:'form:b3',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:menuButton value="m1 - m2 - m3">
                <p:menuitem id="m1" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                    value="m1" />
                <p:menuitem id="m2" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m2" />
                <p:menuitem id="m3" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m3" />
            </p:menuButton>

            <div>
                <p:commandButton type="button" value="hack m1" onclick="PrimeFaces.ab({s:'form:m1',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m2" onclick="PrimeFaces.ab({s:'form:m2',p:'form',u:'form',f:'form'})"
                    style="background:salmon" />
                <p:commandButton type="button" value="hack m3" onclick="PrimeFaces.ab({s:'form:m3',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:menuButton value="m4 - m5 - m6" disabled="true">
                <p:menuitem id="m4" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                    value="m4" />
                <p:menuitem id="m5" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m5" />
                <p:menuitem id="m6" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m6" />
            </p:menuButton>

            <div>
                <p:commandButton type="button" value="hack m4" onclick="PrimeFaces.ab({s:'form:m4',p:'form',u:'form',f:'form'})"
                    style="background:orange" />
                <p:commandButton type="button" value="hack m5" onclick="PrimeFaces.ab({s:'form:m5',p:'form',u:'form',f:'form'})"
                    style="background:salmon" />
                <p:commandButton type="button" value="hack m6" onclick="PrimeFaces.ab({s:'form:m6',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:menuButton value="m7 - m8- m9" rendered="false">
                <p:menuitem id="m7" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                    value="m7" />
                <p:menuitem id="m8" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m8" />
                <p:menuitem id="m9" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m9" />
            </p:menuButton>

            <div>
                <p:commandButton type="button" value="hack m7" onclick="PrimeFaces.ab({s:'form:m7',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m8" onclick="PrimeFaces.ab({s:'form:m8',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m9" onclick="PrimeFaces.ab({s:'form:m9',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:splitButton id="b4" value="m10 - m11 - m12" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                update="timestampPanel">
                <p:menuitem id="m10" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                    value="m10" />
                <p:menuitem id="m11" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m11" />
                <p:menuitem id="m12" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m12" />
            </p:splitButton>

            <div>
                <p:commandButton type="button" value="hack b4" onclick="PrimeFaces.ab({s:'form:b4',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m10" onclick="PrimeFaces.ab({s:'form:m10',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m11" onclick="PrimeFaces.ab({s:'form:m11',p:'form',u:'form',f:'form'})"
                    style="background:salmon" />
                <p:commandButton type="button" value="hack m12" onclick="PrimeFaces.ab({s:'form:m12',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:splitButton disabled="true" id="b5" value="m13 - m14 - m15" action="#{testMenuitemBean.updateTimestamp}"
                process="@form" update="timestampPanel">
                <p:menuitem id="m13" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                    value="m13" />
                <p:menuitem id="m14" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m14" />
                <p:menuitem id="m15" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m15" />
            </p:splitButton>

            <div>
                <p:commandButton type="button" value="hack b5" onclick="PrimeFaces.ab({s:'form:b5',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m13" onclick="PrimeFaces.ab({s:'form:m13',p:'form',u:'form',f:'form'})"
                    style="background:orange" />
                <p:commandButton type="button" value="hack m14" onclick="PrimeFaces.ab({s:'form:m14',p:'form',u:'form',f:'form'})"
                    style="background:salmon" />
                <p:commandButton type="button" value="hack m15" onclick="PrimeFaces.ab({s:'form:m15',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>


        <h:panelGroup>
            <p:splitButton rendered="false" id="b6" value="m16 - m17 - m18" action="#{testMenuitemBean.updateTimestamp}"
                process="@form" update="timestampPanel">
                <p:menuitem id="m16" action="#{testMenuitemBean.updateTimestamp}" process="@form" update="timestampPanel"
                    value="m16" />
                <p:menuitem id="m17" disabled="true" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m17" />
                <p:menuitem id="m18" rendered="false" action="#{testMenuitemBean.updateTimestamp}" process="@form"
                    update="timestampPanel" value="m18" />
            </p:splitButton>

            <div>
                <p:commandButton type="button" value="hack b6" onclick="PrimeFaces.ab({s:'form:b6',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m16" onclick="PrimeFaces.ab({s:'form:m16',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m17" onclick="PrimeFaces.ab({s:'form:m17',p:'form',u:'form',f:'form'})" />
                <p:commandButton type="button" value="hack m18" onclick="PrimeFaces.ab({s:'form:m18',p:'form',u:'form',f:'form'})" />
            </div>
        </h:panelGroup>
    </p:panelGrid>
</h:form>

And the following it’s the rendering

test-page-screen

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 the isDisabled() check:

@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);
}

To prevent the unwanted behavior while having no performance impact (we got a little speedup instead), we should add the AbstractMenu.processDecodes() adding just the isDisabled() check:

public void processDecodes(FacesContext context) {
    if (context == null) {
        throw new NullPointerException();
    }

    // Skip processing if our rendered flag is false or if we are disabled
    if (!isRendered() || isDisabled()) { // <------- added disabled check
        return;
    }

    pushComponentToEL(context, null);

    // Process all facets and children of this component
    Iterator kids = getFacetsAndChildren();
    while (kids.hasNext()) {
        UIComponent kid = (UIComponent) kids.next();
        kid.processDecodes(context);
    }

    // Process this component itself
    try  {
        decode(context);
    } catch (RuntimeException e) {
        context.renderResponse();
        throw e;
    } finally  {
        popComponentFromEL(context);
    }
}

The downsides of this approach are:

  • it isn’t global
  • it has to be applied to each component that can be disabled and that can contain UICommands (i.e. Tab component is another one that’s impacted)
  • it may lead to side-effects
0reactions
mellowarecommented, Feb 16, 2020

Yep open another issue. We will close this one with just the simply fix of #1 checking for disabled in UIMenuItem.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to Recognise which menuItem is disabled?
in onCreateOptionsMenu() store the menu into a local class field, and then to check if a certain menu item is enabled/disabled use isEnabled ......
Read more >
wwdc2022-10049 | Apple Developer Forums
I am using UIMenuControllerWillShowMenuNotification in my app. but in iOS16 UIMenuController is deprecated. I want to check when menu is open but in...
Read more >
Swift should allow for suppression of warnings, especially ...
The case I just ran into involves the warning "'catch' block is unreachable because no errors are thrown in 'do' block'". Here is...
Read more >
Diff - d723882358..4b3282a5d6 - chromium/src - Git at Google
-172,9 +172,11 @@ void SearchBoxView::UpdateSearchIcon() { const ... ErrorMessages) { - // Error strings should not be modified in case of success.
Read more >
Lightning Aura Components Developer Guide
eval() Function is Limited by Lightning Locker . ... that you've created when used in Lightning markup, but not in expressions or JavaScript....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found