spring-projects/spring-security · critical · CycleInRoleHierarchyException
Exception thrown because of a cycle in the role hierarchy…
Error message
Exception thrown because of a cycle in the role hierarchy definition!
What it means
RoleHierarchyImpl.buildRolesReachableInOneOrMoreStepsMap expands each role through the hierarchy; while walking reachable roles it detects when a role reaches itself, meaning the hierarchy definition contains a cycle (e.g. ROLE_A > ROLE_B > ROLE_A). It throws CycleInRoleHierarchyException at construction time, so the application fails to start.
Solutions
- Remove the cyclic mapping from the hierarchy string so roles form a DAG (each role reachable only 'downward')
- Log/print the hierarchy at startup and trace the loop among the roles named in the exception
- Refactor mutually dependent roles into a common parent role instead of circular references
- Add a unit test calling new RoleHierarchyImpl(hierarchyString) for every shipped hierarchy to fail fast on cycles
Example fix
// before String hierarchy = "ROLE_A > ROLE_B\nROLE_B > ROLE_C\nROLE_C > ROLE_A"; // cycle // after String hierarchy = "ROLE_A > ROLE_B\nROLE_A > ROLE_C";
Defensive patterns
Strategy: validation
Validate before calling
Set<String> seen = new HashSet<>();
for (String[] line : mappings) { String from = line[0], to = line[1];
if (from.equals(to) || createsBackEdge(from, to)) throw new IllegalArgumentException("cycle in role hierarchy: " + from + " -> " + to); } Type guard
null
Try / catch
try { new RoleHierarchyImpl(hierarchyString); }
catch (CycleInRoleHierarchyException e) { failFast("Fix role hierarchy cycle before startup"); } Prevention
- Unit-test every hierarchy string at build time
- Keep role edges strictly one-directional (senior > junior)
- Reject self-references when building hierarchies programmatically
- Review merged hierarchy config from multiple modules for loops
When it happens
Trigger: Defining a role hierarchy string where roles reference each other in a loop, directly (ROLE_A > ROLE_A) or transitively (ROLE_A > ROLE_B > ROLE_C > ROLE_A), typically via setHierarchy(...) or the role-hierarchy configuration property.
Common situations: Copy-pasted or merged hierarchy definitions in application.yml/security config; programmatically built hierarchies appending mappings that close a loop; multi-module setups where two modules each add inverse mappings.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- A filter chain that matches any request
- authorizationManagerFactory must be an instance of…
- Can not set rememberMeCookieName and custom…
- Cannot apply to already built object
- Cannot configure both a CorsConfigurationSource and a…
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/2a72944297d02528.
Report an issue: GitHub.
Appendix: source
Thrown at core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java:221
* CycleInRoleHierarchyException if a cycle in the role hierarchy definition is
* detected)
*/
private static Map<String, Set<GrantedAuthority>> buildRolesReachableInOneOrMoreStepsMap(
Map<String, Set<GrantedAuthority>> hierarchy) {
Map<String, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = new HashMap<>();
// iterate over all higher roles from rolesReachableInOneStepMap
for (String roleName : hierarchy.keySet()) {
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(hierarchy.get(roleName));
Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
while (!rolesToVisitSet.isEmpty()) {
// take a role from the rolesToVisit set
GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
rolesToVisitSet.remove(lowerRole);
if (!visitedRolesSet.add(lowerRole) || !hierarchy.containsKey(lowerRole.getAuthority())) {
continue; // Already visited role or role with missing hierarchy
}
else if (roleName.equals(lowerRole.getAuthority())) {
throw new CycleInRoleHierarchyException();
}
rolesToVisitSet.addAll(hierarchy.get(lowerRole.getAuthority()));
}
rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
logger.debug(LogMessage.format(
"buildRolesReachableInOneOrMoreStepsMap() - From role %s one can reach %s in one or more steps.",
roleName, visitedRolesSet));
}
return rolesReachableInOneOrMoreStepsMap;
}
/**
* Builder class for constructing a {@link RoleHierarchyImpl} based on a hierarchical
* role structure.
*
* @author Federico Herrera
* @since 6.3
*/View on GitHub (pinned to 96852e8860)