spring-projects / spring-projects/spring-security

ACL parent cycle causes infinite recursion in BasicLookupStrategy.convert()

Open
#19,513 0 comments 0 reactions 1 assignee View on GitHub

@jzheaux is already working on this.

Since Aug 7, 2026.

in: acl type: bug
Dominant language
Java
Stars
9.6k
Forks
6.3k
Avg merge
2d 11h
Merged PRs (30d)
52

Description

AclImpl.setParent() only rejects direct self-parenting (newParent.equals(this)); it does not detect indirect cycles (A → B → A, or longer chains). Once such a cycle is persisted via JdbcMutableAclService.updateAcl(), every subsequent lookup of any ACL in the cycle recurses forever.

Where:

  • AclImpl.setParent(Acl) (acl/src/main/java/org/springframework/security/acls/domain/AclImpl.java) only guards newParent.equals(this) — no ancestor walk.
  • JdbcMutableAclService.updateObjectIdentity(MutableAcl) (acl/src/main/java/org/springframework/security/acls/jdbc/JdbcMutableAclService.java) writes parentId with no cycle check.
  • BasicLookupStrategy.convert(...) (acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java, ~lines 386-431) recurses on StubAclParent ids with no visited-set, so a stored cycle causes unconditional infinite recursion / StackOverflowError on every future read. JdbcMutableAclService.clearCacheIncludingChildren(...) has the same unguarded recursive shape.

Repro sketch:

  1. User owns (or has CHANGE_GENERAL on) two ACL-protected objects A and B.
  2. aclA.setParent(aclB); aclService.updateAcl(aclA);
  3. aclB.setParent(aclA); aclService.updateAcl(aclB);
  4. Any subsequent aclService.readAclById(...) for A or B recurses forever.

Suggested fix shape:

  • In AclImpl.setParent(Acl), walk newParent's ancestor chain via getParentAcl() and reject (IllegalArgumentException, same unchecked pattern as the existing self-parent Assert.isTrue) if this is reachable.
  • In JdbcMutableAclService.updateObjectIdentity(MutableAcl), walk the proposed parent's stored ancestry before writing parentId and reject on a match, for the case where the in-memory check is bypassed (e.g. a custom MutableAcl implementation, or direct DB manipulation).
  • Add a visited-Set guard in BasicLookupStrategy.convert(...) and clearCacheIncludingChildren(...) so that already-corrupt data (e.g. from before this fix, or written through a path that skips the new checks) fails fast with a clear exception instead of a StackOverflowError.

This should be additive/non-breaking: a stored parent cycle is already guaranteed to crash every future read of the affected ACLs, so no working application depends on being able to create one. Rejecting the write earlier, with a clear exception, doesn't remove any capability that currently functions.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.