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
- 01User-controlled object id
/rpc/api formula.setFormulasOfGroup / formula.getCombinedFormulaDataByServerIdsA session-key authenticated caller supplies a system group id or a list of server ids that can name objects in any organization.
- 02Unscoped write lookup
FormulaHandler.setFormulasOfGroup -> ServerGroupFactory.lookupById(Long)The group is fetched by id with no organization constraint and FormulaFactory.saveGroupFormulas performs no ownership check.
- 03Unscoped read lookup
FormulaHandler.getCombinedFormulaDataByServerIds -> FormulaManager.getCombinedFormulaDataForSystemsMinionServerFactory.findMinionsByServerIds resolves the ids without an organization filter and combined formula and pillar data is returned with no per-server permission check.
- 04Missing authorization gate
FormulaHandler.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
- 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.
- Run the handler census below. Compare the organization and permission calls on the scoped sibling methods with the two affected methods.
- 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.
- 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.
- Confirm that the affected policy allows the cross-organization action while the fixed policy denies it.
- 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-*.javaSynthetic 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: ALLOWandfixed: DENYfor the cross-organization case. No organization, group, minion, or XML-RPC request is created.
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.
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.