Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Table of Contents


Overview

Helium and the Helium DSL provides built-in role based authorisation as well as mechanisms for managing app roles that are assigned to users. In some cases, however, a more dynamic system of managing roles is preferable. In such cases an app might use various internal roles of which multiple can be assigned to a user for each "Helium provided" app role.

Since these dynamic roles are not managed by Helium itself but rather by app logic some risks and mitigation strategies need to be considered to ensure users are not allowed to access functionality and data that they are not authorised for.




App Example

The rest of this document will refer to a sample app that demonstrates a minimal example of dynamic, application managed (as apposed to platform managed) roles.

Consider the following code snippets representing an appropriate model for managing of dynamic user roles:

Code Block
titleWebUser Model Object
@Role("Web User")
persistent object WebUser {
    string name;
    string mobile;

    @ManyToMany
    WebUserRole roles via users;
}


Code Block
titleWebUserRole Model Object
persistent object WebUserRole {
    string name;
}

Taking the above into consideration consider the following data representing 4 roles and a single user with three of the four roles assigned to them. Note the data here is generated as part of the automatic / built-in app invitation. This is for demonstration purposes only and should not be considered best practice for general bulk data generation or inserts.

Code Block
titleExample Data Being Generated
@InviteUser
WebUser getWebUser() {

    // Create the default user
    WebUser webUser = WebUser:new();
    webUser.name = "Test User";
    webUser.mobile = "27761112222";
    webUser.save();

    // Create and assign roles to the user
    createRoles(webUser);

    return webUser;
}

void createRoles(WebUser webUser) {

    // Create roles 1 - 3 and assign to the user
    WebUserRole role1 = WebUserRole:new();
    role1.name = "role1";
    role1.users.append(webUser);
    role1.save();

    WebUserRole role2 = WebUserRole:new();
    role2.name = "role2";
    role2.users.append(webUser);
    role2.save();

    WebUserRole role3 = WebUserRole:new();
    role3.name = "role3";
    role3.users.append(webUser);
    role3.save();

    // Create role 4 but do not assign it the the user
    WebUserRole role4 = WebUserRole:new();
    role4.name = "role4";
    role4.save();
}


Code Block
titlePersisted WebUser
helium-app-1=# select * from webuser;
-[ RECORD 1 ]-+-------------------------------------
_id_          | 12e5351f-ff6b-4ac7-9816-bff118a72e84
_tstamp_      | 2021-07-07 16:51:07.824271
name          | Test User
mobile        | 27761112222
_firstnames   | Test
_nickname     | 
_surname      | User
_locale       | pt
_timezone     | Africa/Johannesburg
_tx_id_       | 44267184
_change_type_ | update
_change_seq_  | 15


Code Block
titlePersisted WebUserRole records
helium-app-1=# select * from webuserrole ;
                 _id_                 |          _tstamp_          | name  | _tx_id_  | _change_type_ | _change_seq_ 
--------------------------------------+----------------------------+-------+----------+---------------+--------------
 2c4840f5-3181-433e-a731-f4545237f3f9 | 2021-07-07 16:51:07.249882 | role1 | 44267173 | create        |            4
 f3a7b0af-d2b5-411a-a1a9-94a8c6b48b2a | 2021-07-07 16:51:07.249882 | role2 | 44267173 | create        |            7
 98c6bd22-756c-421f-be73-c175b17f376a | 2021-07-07 16:51:07.249882 | role3 | 44267173 | create        |           10
 fca8fe35-90d9-4076-96b3-a79af35af53b | 2021-07-07 16:51:07.249882 | role4 | 44267173 | create        |           13


Code Block
titleWebUserRole records as linked to the WebUser record
 helium-app-1=# select * from webuser_roles ;
              webuser_fk              |            webuserrole_fk            |          _tstamp_          | _tx_id_  | _change_seq_ 
--------------------------------------+--------------------------------------+----------------------------+----------+--------------
 12e5351f-ff6b-4ac7-9816-bff118a72e84 | 2c4840f5-3181-433e-a731-f4545237f3f9 | 2021-07-07 16:51:07.249882 | 44267173 |            5
 12e5351f-ff6b-4ac7-9816-bff118a72e84 | f3a7b0af-d2b5-411a-a1a9-94a8c6b48b2a | 2021-07-07 16:51:07.249882 | 44267173 |            8
 12e5351f-ff6b-4ac7-9816-bff118a72e84 | 98c6bd22-756c-421f-be73-c175b17f376a | 2021-07-07 16:51:07.249882 | 44267173 |           11

With data model and test data consider the following view and unit:

Code Block
<?xml version="1.0" encoding="UTF-8"?>
<ui xmlns="http://uiprogram.mezzanine.com/View" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://uiprogram.mezzanine.com/View">
    <view label="view_heading.mainmenu" unit="MainMenuUnit" init="init">
	
	<menuitem label="menu_item.mainmenu">
	    <userRole>Web User</userRole>
	</menuitem>
	
	<select label="select.available_roles">
	    <binding variable="selectedRole"/>
	    <collectionSource function="getAvailableRoles">
		<displayAttribute name="name"/>
	    </collectionSource>
	</select>
	
	<submit label="button.update_role" action="updateRoleSelection"/>
	
	<!-- This widget is a placeholder and might represent a menu or other app functionality in practice -->
	<info label="info.current_role_label" value="info.current_role_content"/>
    </view>
</ui>


Code Block
unit MainMenuUnit;

WebUserRole selectedRole;
string currentRoleName;

void init() {
    setViewState();
}

// Get the roles that have been assigned to this user
WebUserRole[] getAvailableRoles() {
    if(Mez:userRole() == "Web User") {
	   WebUser currentUser = WebUser:user();
	   return currentUser.roles;
    }
}


// Set the views state based on the current user and selected roles
void setViewState() {
    
    WebUserRole[] availableRoles = getAvailableRoles();

    // If roles are available but not selected default to the first value
    if(availableRoles.length() > 0 && selectedRole == null) {
	   selectedRole = availableRoles.get(0);
    }

    if(selectedRole != null) {
	   currentRoleName = selectedRole.name;
	   return null;
    }

    currentRoleName = "You either have no roles assigned to you";
    return null;
}


// Submit user role selection
void updateRoleSelection() {
    setViewState();
    return null;
}

The app described above contains a single view with a role dropdown that lists all the roles (as defined in the app logic) that has been assigned to the user. In also contains a submit button to submit the role selection and an info widget for which the state (value being displayed) is determined by the selected role. 




Authorisation Bypass

Although a detailed example will not be provided here the concept is as follows: If the uuid for a role is known, a user, that has access to the application but does not have access to that role, can generate calls to Helium that updates the role selection to one that is not available in his role dropdown. If the view is then reloaded for that menu item the user might have access to functionality for a role that they have not been assigned.

Using this concept and applying it to our example app, it is possible that the user that does not have "role4" assigned to them, can access the app functionality for role4 for given that the id for the object instance (WebUserRole) that  that represents role4 is known:




Mitigation

Fundamentally the authorisation bypass risk highlighted here can be mitigated by using additional validation that can be used to determine what view or application state should be presented to the user. We will describe three mechanism in the Helium DSL that can be used to achieve this.

Dynamically determine the state of the widgets being displayed

In this case we will include additional validation that needs to pass before the view state is determined. The state of the widgets that are displayed  to the user can then be set in such a way that the user does not have access to app functionality that they are not authorised for.

In the case where a widget represents a menu that uses a data table the contents of the table can simply be omitted resulting in a scenario where no further functionality is available to the "unauthorised" user.

In our case we can set the state of the info widget to demonstrate the concept. 

First, consider the additional validation function added to the MainMenuUnit.mez  unit as well as its usage in the setViewState method method:

Code Block
// Validate that the current user has the selected roles assigned to them
bool validateRole() {

    WebUserRole[] availableRoles = getAvailableRoles();

    if(availableRoles.length() == 0) {
        return false;
    }

    foreach(WebUserRole availableRole: availableRoles) {
        if(availableRole.name == selectedRole.name) {
            return true;
        }
    }

    return false;
}


Code Block
// Set the views state based on the current user and selected roles
void setViewState() {
    
    WebUserRole[] availableRoles = getAvailableRoles();

    // If roles are available but not selected default to the first value
    if(availableRoles.length() > 0 && selectedRole == null) {
		selectedRole = availableRoles.get(0);
    }

    if(selectedRole != null && validateRole() == true) {
		currentRoleName = selectedRole.name;
		return null;
    }

    currentRoleName = "You either have no roles assigned to you or you are trying to access functionality for which you are not authorised";
    return null;
}

Attempting to access the functionality using a role for which the user is not authorised will now result in the following in our example app:

Dynamically hide widgets

Similarly to the previous example, the same validation can be used to determine whether view widgets should be displayed at all. In the example app this is achieved by modifying our setViewState method  method to set a visibility flag for all widgets on the view and then use the visibility bindings on the view itself:


Code Block
bool displayWidgets;

.
.
.


// Set the views state based on the current user and selected roles
void setViewState() {
    
    WebUserRole[] availableRoles = getAvailableRoles();

    // If roles are available but not selected default to the first value
    if(availableRoles.length() > 0 && selectedRole == null) {
	   selectedRole = availableRoles.get(0);
    }

    if(selectedRole != null && validateRole() == true) {
	currentRoleName = selectedRole.name;
	   displayWidgets = true;
	   return null;
    }

    displayWidgets = false;
    return null;
}

.
.
.


Code Block
<?xml version="1.0" encoding="UTF-8"?>
<ui xmlns="http://uiprogram.mezzanine.com/View" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://uiprogram.mezzanine.com/View">
    <view label="view_heading.mainmenu" unit="MainMenuUnit" init="init">
    
        <menuitem label="menu_item.mainmenu">
            <userRole>Web User</userRole>
        </menuitem>
        
        <select label="select.available_roles">
            <visible variable="displayWidgets"/>
            <binding variable="selectedRole"/>
            <collectionSource function="getAvailableRoles">
            <displayAttribute name="name"/>
            </collectionSource>
        </select>
        
        <submit label="button.update_role" action="updateRoleSelection">
            <visible variable="displayWidgets"/>
        </submit>
        
        <info label="info.current_role_label" value="info.current_role_content">
            <visible variable="displayWidgets"/>
        </info>
    </view>
</ui>

With this in place in our example app attempting unauthorised access to the view will result in the following:

In this case Helium attempts to load the view but fails due to the fact that no widgets are visible on the view. If the visibility binding was not applied to all widgets on the view, the view would load while hiding those widgets to which the invisibility bindings were applied.

More information on visibility binding can be found here.

Using built-in dynamic menu items

The final method that will be discussed here is to use the same validation as before and then use Helium's built-in dynamic menu item functionality to prevent unauthorised users from accessing the built-in app menu items entirely.

To implement this in our example app we modify the menu item on the view as follows:

Code Block
<menuitem>
	<dynamicUserRoles function="getRolesForMainMenu"/>
	<dynamicLabel function="getLabel"/>
</menuitem>

We also add the accompanying methods to the MainMenuUnit.mez unit:

Code Block
// Authorise dynamic rendering of menu items
DSL_ROLES[] getRolesForMainMenu() {
    
    setViewState();
    DSL_ROLES[] roles;

    if(validateRole() == true) {
		roles.append(DSL_ROLES.Web_User);
    }

    return roles;
}

string getLabel() {
    return String:translate("menu_item.mainmenu");
}

Attempting to access the functionality using a role for which the user is not authorised will now result in the following in our example app:

In this case Helium attempts to find a menu item that is applicable to the user in order to load the associated view. Due to the validation, however, getRolesForMainMenu returns  getRolesForMainMenu returns an empty list and thus no menu item is available to the unauthorised user.

This will be accompanied by the following message in the logs:

Code Block
Caused by: com.mezzanine.program.web.exception.SolutionException: There are no menu items for the current role, Web User. At least one view with a menu item is required.

More information on dynamic menu items can be found here.




Conclusion & Resources

In conclusion this document described how app managed dynamic roles can expose a security exploits vulnerability by means of authorisation bypass.

If was then We also demonstrated how additional validation logic could be used in conjunction with general widget state, widget visibility bindings and dynamic menu items to deny access to app functionality for unauthorised users.

Also see the following resources: