79832198

Date: 2025-11-28 02:15:21
Score: 0.5
Natty:
Report link

Bypassing tenant isolation in Spring and Hibernate using isRoot

You can switch to the root tenant. That way, the session can see data of all tenants.

Define a root tenant ID. For example, if you are using UUID:

public static final UUID ROOT_TENANT_ID = UUID.fromString("00000000-0000-0000-0000-000000000000");

In your implementation of the CurrentTenantIdentifierResolver interface, override the isRoot method:

  @Override
  public boolean isRoot(UUID tenantId) {
    return ROOT_TENANT_ID.equals(tenantId);
  }

When that method returns true, Hibernate does not match the value of the tenantId field to the tenantId of the session, and the session can read any other tenant’s data.

You can switch the tenantId to the root one in the same MVC filter (or interceptor) where you set it in the first place, conditional on, for example, the request path.

Or, you can change tenantId before entering the method with Spring AOP.

Create a custom annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AsRootTenant {
}

Annotate your business method:

@Transactional
@AsRootTenant
public void someServiceMethod(UUID originalTenantId, UUID secondTenantId) {
   Information info = repository.getInfoByTenantId(originalTenantId);   
   Information info2 = repository.getInfoByTenantId(someOtherTenantId); 
}

Create an aspect to replace the tenantId before entering the annotated method. For example, if you are using ScopedValue to store tenantId as in this article, you can do:

public class AsRootTenantAspect implements Ordered {
  @Around("@annotation(com.example.AsRootTenant)")
  public Object aroundAnnotatedMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    return ScopedValue.where(TenantIdHolder.scopedTenantId, ROOT_TENANT_ID).call(
        () -> joinPoint.proceed());
  }
}

If you are using ThreadLocal instead, you could set its value inside the aroundAnnotatedMethod, restoring it before exiting:

  @Around("@annotation(com.example.AsRootTenant)")
  public Object aroundAnnotatedMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    UUID originalTenantId = tenantIdHolder.getTenantId();
    try {
      tenantIdHolder.setTenantId(ROOT_TENANT_ID);
      return joinPoint.proceed();
    } finally {
      tenantIdHolder.setTenantId(originalTenantId);
    }
  }

You need the AsRootTenantAspect to override getOrder so that the aspect could be reliably called before the Spring TransactionInterceptor (which has the lowest priority Integer.MAX_VALUE).

  @Override
  public int getOrder() {
    return Integer.MAX_VALUE  - 1;
  }

You will need to ensure that no prior method down in the call stack is annotated with @Transactional. Also, you have to disable open-in-view in your application.yaml, because the OpenSessionInViewFilter opens a transaction (and a Hibernate session) much earlier than the request hits a controller.

spring.jpa.open-in-view:false

You can not only read, but also update fields of objects belonging to different tenants this way, and the change will be persisted. However, changes to the field annotated with @TenantId will be ignored.

Reasons:
  • Blacklisted phrase (1): this article
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Andrei Litvinov