Audit code for multi-tenant security vulnerabilities and data isolation issues
Audit for tenant isolation and security vulnerabilities in: $ARGUMENTS
Determine if entity extends TenantBaseEntity:
Dangerous methods requiring verification:
repository.findAll()repository.findById()repository.findAllById()@Query without WHERE church_id = :churchIdFor EACH service method that queries tenant-scoped data:
@Transactional or @Transactional(readOnly = true)?findAll() calls - is filter properly enabled?findById() - is returned entity validated against current church?@Query - does it include WHERE e.church.id = :churchId?Pattern 1: Missing @Transactional
// ❌ VULNERABLE - Returns ALL churches' data!
public List<GoalResponse> getAllGoals() {
return goalRepository.findAll().stream()
.map(GoalResponse::fromEntity)
.collect(Collectors.toList());
}
// ✅ SECURE
@Transactional(readOnly = true)
public List<GoalResponse> getAllGoals() {
return goalRepository.findAll().stream()...
}
Pattern 2: findAll() in private methods
// ❌ VULNERABLE
private List<Member> determineRecipients(Request request) {
return memberRepository.findAll(); // ALL churches!
}
// ✅ SECURE
private List<Member> determineRecipients(Request request) {
Long churchId = TenantContext.getCurrentChurchId();
return memberRepository.findByChurchId(churchId);
}
Pattern 3: Missing church validation on findById
// ❌ VULNERABLE - Could return entity from another church
public VisitorResponse getVisitor(Long id) {
Visitor visitor = visitorRepository.findById(id).orElseThrow(...);
return VisitorMapper.toVisitorResponse(visitor);
}
// ✅ SECURE
@Transactional(readOnly = true)
public VisitorResponse getVisitor(Long id) {
Visitor visitor = visitorRepository.findById(id).orElseThrow(...);
Long churchId = TenantContext.getCurrentChurchId();
if (!visitor.getChurchId().equals(churchId)) {
throw new AccessDeniedException("Access denied");
}
return VisitorMapper.toVisitorResponse(visitor);
}
Pattern 4: Custom @Query without church filter
// ❌ VULNERABLE - Hibernate filter DOES NOT apply to @Query!
@Query("SELECT COUNT(v) FROM Visitor v WHERE v.lastVisitDate BETWEEN :start AND :end")
Long countVisitors(...); // Counts ALL churches!
// ✅ SECURE
@Query("SELECT COUNT(v) FROM Visitor v WHERE v.church.id = :churchId AND v.lastVisitDate BETWEEN :start AND :end")
Long countVisitors(@Param("churchId") Long churchId, ...);
Tenant-Scoped (require @Transactional): Member, Fellowship, AttendanceSession, AttendanceReminder, Visitor, Goal, Insight, Complaint, Event, Donation, Pledge, CounselingSession, PrayerRequest, Visit
Platform-Level (SUPERADMIN only): Church, SubscriptionPlan, PartnershipCode, StorageAddon
Special Cases: User - extends BaseEntity, NOT TenantBaseEntity (manual filtering required)
✅ Secure examples to follow:
MemberService.getAllMembers() - @Transactional + TenantContextFellowshipService.getAllFellowships() - @Transactional + explicit churchIdUserService.getAllUsers() - Manual filtering with role checkAfter auditing, report: