Liquidfiles Privilege Escalation

An admin in a secondary domain can escalate themselves to sysadmin in Liquidfiles.

Liquidfiles Privilege Escalation

Liquidfiles has a feature called Liquidfiles Domains where a single instance can host multiple instances on separate URLs that are logically seperated.

Each Domain will have their own users, groups, configuration, certificate, branding and so on.
LiquidFiles Domains | LiquidFiles Documentation
What are LiquidFiles domains and when to use them?

What we found was that it's possible to takeover the 'default domain' and become a sysadmin if you've been provided an admin account in any secondary domain.

Admin -> Sysadmin Privilege Escalation

💡
Tracked as CVE-2026-12673.

In LiquidFiles, administrative privilege levels are represented by the following numeric values:

  • 5 -> SysAdmin
  • 4 -> Domain Admin
  • 3 -> Admin

Within the default domain, the highest available privilege level is SysAdmin. In secondary domains, the highest available privilege level is Domain Admin.

The following controller logic is intended to prevent administrators from creating or modifying groups with higher privileges than they are authorised to assign. We've spotted a

app/controllers/admin/groups_controller.rb
  
  def limit_create_for_admins
    return if Current.user.sysadmin?
    if admin_domain.default?
      return unless params[:group][:sysadmin].to_b || params[:group][:admin_level].to_i == 5
      redirect_to new_admin_group_url, alert: 'You are not permitted to add a sysadmin group.'
    else
      return unless params[:group][:domain_admin].to_b || params[:group][:admin_level].to_i == 4
      redirect_to new_admin_group_url, alert: 'You are not permitted to add a Domain Admin group.'
    end
  end

  def limit_update_for_admins
    return if Current.user.sysadmin?
    if admin_domain.default?
      return unless params[:group][:sysadmin].to_b || params[:group][:admin_level].to_i == 5
      redirect_to edit_admin_group_url(id: params[:id]),
                  alert: 'You are not permitted to change group to a sysadmin group.'
    else
      return unless params[:group][:domain_admin].to_b || params[:group][:admin_level].to_i == 4
      redirect_to edit_admin_group_url(id: params[:id]),
                  alert: 'You are not permitted to change group to a Domain Admin group.'
    end
  end

For secondary domains, this logic correctly prevents Admins from creating or modifying groups with the Domain Admin privilege level (admin_level = 4). However, it does not prevent the creation or modification of groups with the SysAdmin privilege level (admin_level = 5). There's also some leakage in the Default Tenant.

As a result, an Admin in a secondary domain can create a new group or update an existing group to have SysAdmin privileges.

To demonstrate this we can setup 2 Domains:

Then create a domain admin and a secondary user that we will escalate to system admin. Login as the Domain Admin.

As the domain admin create a Test Group normally and assign our target user to the group.

Edit the Test Group and intercept the HTTP Request.

Change the group[admin_level] to 5 in the request to POST /admin/groups/groupslug. PATCH requests will also work.

Our original Domain Admin will now see this message.

Our secondary user is now a sysadmin.

The fix they applied looks like this:

  def limit_create_for_admins
    return if permitted_admin_level?
    redirect_to new_admin_group_url,
                alert: "You are not permitted to add a " \
                       "#{Group.admin_level_name(requested_admin_level)} group."
  end

  def limit_update_for_admins
    return if permitted_admin_level?
    redirect_to edit_admin_group_url(id: params[:id]),
                alert: "You are not permitted to change group to a " \
                       "#{Group.admin_level_name(requested_admin_level)} group."
  end

  # Admins must not grant a group a higher privilege level than they hold themselves.
  # Without this a Domain Admin could escalate a group (and its users) to System Admin,
  # or an Admin could escalate to Domain Admin. Sysadmins are unrestricted.
  def permitted_admin_level?
    return true if Current.user.sysadmin?
    requested_admin_level <= Current.user.group.admin_level
  end

If you're building software, having it tested by an independent security team can surface these kinds of vulnerabilities before they're exploited in the wild.

An Alternative (Broken) Privilege Escalation

Digging a bit deeper, we also found an alternative way to escalate privileges using the GUI.

If a custom group has users assigned to it, a Delete button is shown in the UI.

If you click the delete button on the group there's a nice option to move those users before deleting the group.

As an admin you could create a user, add them to a group, and then escalate them to a Domain Admin using this feature.

Unfortunately, the code actually moves the user but then it looks like there's a bug and the deletion of the group deletes the moved users as well. This feature must've never been QA'ed!

A very small part of me wanted to report the bug to get the bug fixed, then report it as a security issue after.
 permitted_parameters :create, move_to_group_id: Parameters.string
  def create
    user_count = @group.users.count 

--- Loads into memory and caches the users
    @group.users.each do |user| 
      Applog.debug 'Deleting group, User moved to new group', {
        user_id: user.slug,
        user_email: user.email,
        old_group_id: @group.slug,
        old_group_name: @group.name,
        new_group_id: @new_group.slug,
        new_group_name: @new_group.name
      }

--- User group updated
      user.update group_id: @new_group.id
    end
    Applog.info %{Group Deleted, #{pluralize(user_count, 'user')} moved to new group}, {
      old_group_id: @group.slug,
      old_group_name: @group.name,
      new_group_id: @new_group.slug,
      new_group_name: @new_group.name
    }

--- This deletes the group and cached users in that group
    @group.destroy
    redirect_to admin_groups_url, notice: %{#{pluralize(user_count, 'user')} moved to
      the #{@new_group.name} group. The group #{@group.name} has been deleted.}.mlstrip
  end