CVE case study

CVE-2026-63007: Two Uyuni Formula Methods Skipped the Cross-Org Check

Two public formula XML-RPC methods in Uyuni resolved a server group or a set of servers by id and omitted the organization and permission check that every sibling method enforces. Any authenticated user could read and tamper with formula configuration belonging to another organization.

Severity
High (8.1)
Scoring
CVSS 3.1
Weakness
CWE-639 · CWE-862
Affected
spacewalk-java before 5.2.19-0, 5.1.28-0, and 4.3.92-0
Remediation state
Upgrade to spacewalk-java 5.2.19-0, 5.1.28-0, or 4.3.92-0
Advisory published
20 Jul 2026

Official vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Why it matters

In Uyuni the organization is the tenant boundary. Two formula methods reachable over /rpc/api with session-key authentication resolved their target object by id alone and never compared the caller's organization with the object's owner.

formula.setFormulasOfGroup assigned or removed Salt formulas on any server group and then forced a pillar refresh, pushing the changed configuration to that group's managed minions. A low-privileged user in one organization could alter another organization's system configuration.

formula.getCombinedFormulaDataByServerIds returned combined formula and pillar data for any server id. Formula values are Salt pillar data and routinely carry secrets, so this disclosed another organization's confidential configuration by id enumeration.

How I found it

I read the formula XML-RPC namespace in spacewalk-java and listed every handler method that resolves a server group or a set of servers, then checked which ones call the shared organization and permission helpers.

Most methods scoped their lookup to the caller's organization and called a FormulaUtil.ensureUserHasPermissions* helper. Two did not: setFormulasOfGroup resolved the group with the unscoped ServerGroupFactory.lookupById and saved formulas with no ownership check, and getCombinedFormulaDataByServerIds resolved servers with MinionServerFactory.findMinionsByServerIds and returned combined pillar data with no per-server check.

I confirmed the sibling methods were the intended pattern. getFormulasByGroupId and setGroupFormulaData use ServerGroupFactory.lookupByIdAndOrg plus ensureUserHasPermissionsOnServerGroup, and setFormulasOfServer uses ensureUserHasPermissionsOnServer. Only the two identified methods skipped the check, which pointed to a missing gate rather than an intended capability. A synthetic local policy model reproduced the cross-organization decision without touching a live Uyuni instance, and the published fix confirmed the two missing calls.

Root cause

setFormulasOfGroup resolved its group with ServerGroupFactory.lookupById, an unscoped WHERE id = :id lookup, and FormulaFactory.saveGroupFormulas performed no ownership check. The handler was the only intended authorization gate and it was missing the check.

getCombinedFormulaDataByServerIds passed its ids to MinionServerFactory.findMinionsByServerIds, which selects minions by id with no organization filter, and returned their data with no per-server permission check.

Every other formula method that touches a group or a server scopes the lookup to the caller's organization and calls a permission helper. getFormulasByGroupId and setGroupFormulaData use ServerGroupFactory.lookupByIdAndOrg with FormulaUtil.ensureUserHasPermissionsOnServerGroup, and setFormulasOfServer uses ensureUserHasPermissionsOnServer. Only these two methods skipped the pattern.

Source-to-sink trace

  1. 01
    User-controlled object id/rpc/api formula.setFormulasOfGroup / formula.getCombinedFormulaDataByServerIds

    A session-key authenticated caller supplies a system group id or a list of server ids that can name objects in any organization.

  2. 02
    Unscoped write lookupFormulaHandler.setFormulasOfGroup -> ServerGroupFactory.lookupById(Long)

    The group is fetched by id with no organization constraint and FormulaFactory.saveGroupFormulas performs no ownership check.

  3. 03
    Unscoped read lookupFormulaHandler.getCombinedFormulaDataByServerIds -> FormulaManager.getCombinedFormulaDataForSystems

    MinionServerFactory.findMinionsByServerIds resolves the ids without an organization filter and combined formula and pillar data is returned with no per-server permission check.

  4. 04
    Missing authorization gateFormulaHandler.java (formula namespace handler)

    The handler is the only intended authorization gate for these XML-RPC methods, and it omits the FormulaUtil.ensureUserHasPermissions* call that every sibling formula method makes.

Safe proof of concept

Prerequisites

  • Git and Python 3.
  • Network access only to clone the public uyuni repository; every analysis command after cloning runs locally.

Step-by-step reproduction

  1. Clone the public uyuni repository into a unique temporary directory and export FormulaHandler.java and FormulaManager.java at the pre-fix and fixed commits of the vendor patch.
  2. Run the handler census below. Compare the organization and permission calls on the scoped sibling methods with the two affected methods.
  3. Confirm that the affected setFormulasOfGroup resolves its group with the unscoped lookupById and that getCombinedFormulaDataForSystems resolves servers with findMinionsByServerIds, neither calling a FormulaUtil.ensureUserHasPermissions* helper.
  4. Run the synthetic policy model using a caller in one organization and an object owned by another. It represents the missing cross-organization decision without issuing a real XML-RPC call.
  5. Confirm that the affected policy allows the cross-organization action while the fixed policy denies it.
  6. Inspect the fixed commits and verify that setFormulasOfGroup now calls ensureUserHasPermissionsOnServerGroup and that getCombinedFormulaDataForSystems now calls ensureUserHasPermissionsOnServer for each resolved minion.

Compare the affected and fixed formula authorization gates

LAB_DIR=$(mktemp -d "\${TMPDIR:-/tmp}/uyuni-formula-cve.XXXXXX")
git clone https://github.com/uyuni-project/uyuni.git "$LAB_DIR/uyuni"

HANDLER=java/core/src/main/java/com/redhat/rhn/frontend/xmlrpc/formula/FormulaHandler.java
MANAGER=java/core/src/main/java/com/redhat/rhn/manager/formula/FormulaManager.java

# c9e4bb1 = pre-fix master, 3dac733 = merge of fix PR #12209
git -C "$LAB_DIR/uyuni" show c9e4bb1cbc3e:"$HANDLER" > "$LAB_DIR/handler-affected.java"
git -C "$LAB_DIR/uyuni" show 3dac733f80c6:"$HANDLER" > "$LAB_DIR/handler-fixed.java"
git -C "$LAB_DIR/uyuni" show c9e4bb1cbc3e:"$MANAGER" > "$LAB_DIR/manager-affected.java"
git -C "$LAB_DIR/uyuni" show 3dac733f80c6:"$MANAGER" > "$LAB_DIR/manager-fixed.java"

rg -n -C 4 \
  'setFormulasOfGroup|getCombinedFormulaDataByServerIds|getCombinedFormulaDataForSystems|lookupById|lookupByIdAndOrg|findMinionsByServerIds|ensureUserHasPermissionsOn(Server|ServerGroup)' \
  "$LAB_DIR"/handler-*.java "$LAB_DIR"/manager-*.java

Synthetic cross-organization policy proof

from dataclasses import dataclass

@dataclass
class Org:
    id: int

@dataclass
class Caller:
    name: str
    org: Org

@dataclass
class Target:      # a server group or a minion
    kind: str
    org: Org

# Affected: the handler resolves the object by id only and never
# compares the caller's organization with the object's owner.
def affected(caller, target):
    return True

# Fixed: FormulaUtil.ensureUserHasPermissionsOn(ServerGroup|Server)
# validates that the caller may act on the object in its own org.
def fixed(caller, target):
    return caller.org.id == target.org.id

org_a, org_b = Org(1), Org(2)
attacker = Caller("org-a-user", org_a)
victim_group = Target("server_group", org_b)

print("affected:", "ALLOW" if affected(attacker, victim_group) else "DENY")
print("fixed:   ", "ALLOW" if fixed(attacker, victim_group) else "DENY")

Expected evidence

  • The affected handler shows setFormulasOfGroup calling ServerGroupFactory.lookupById and getCombinedFormulaDataForSystems calling MinionServerFactory.findMinionsByServerIds, neither followed by a FormulaUtil.ensureUserHasPermissions* call.
  • The scoped sibling methods in the same files show the lookupByIdAndOrg and ensureUserHasPermissions* pattern, confirming the two methods are the exception.
  • The synthetic model prints affected: ALLOW and fixed: DENY for the cross-organization case. No organization, group, minion, or XML-RPC request is created.
Negative control

Set the caller and the target to the same organization. The fixed model allows that legitimate same-org management case while still denying the cross-organization transition.

Fixed-version re-test

On 5.2.19-0, 5.1.28-0, or 4.3.92-0, a cross-organization setFormulasOfGroup or getCombinedFormulaDataByServerIds call by a user who lacks rights on the object must fail the permission check. Same-organization actions by an authorized user must continue to work.

Impact

An authenticated user in one organization could tamper with formula assignments on another organization's server groups and trigger a pillar refresh that applied the change to that organization's minions. That is a cross-tenant integrity and configuration-control impact.

The same class of access could read combined formula and pillar data for servers in another organization by supplying their ids, disclosing configuration values that commonly include secrets.

The official vector is AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N, High at 8.1. It requires a low-privileged authenticated account, no user interaction, and no elevated role, which matches a multi-tenant Uyuni deployment.

Fix and retest

Upgrade to spacewalk-java 5.2.19-0, 5.1.28-0, or 4.3.92-0. The fix adds FormulaUtil.ensureUserHasPermissionsOnServerGroup(loggedInUser, group) to setFormulasOfGroup and a per-minion FormulaUtil.ensureUserHasPermissionsOnServer(user, minion) loop to getCombinedFormulaDataForSystems, matching the sibling methods.

Retest both paths across an organization boundary. A caller who lacks rights on the target group or server must be rejected on both the write and the read method, while same-organization actions by an authorized user must continue to work.

Engineering lesson

When a family of methods shares an authorization pattern, the outlier is the bug. An id-based lookup at a tenant boundary needs an ownership or permission check on every method, not on most of them; a shared helper that each sibling is required to call makes the missing one visible in review.

References

From evidence to assessment

Need this kind of risk assessed in your environment?

Authorized engagements begin with a fit check, written scope, and the outcome your team needs from the assessment.

Explore security services Start an assessment enquiry