Bola Idor in Spring Boot with Cockroachdb
Bola Idor in Spring Boot with Cockroachdb — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API exposes one user’s resource by allowing direct manipulation of an identifier (ID) without verifying ownership. In a Spring Boot application backed by CockroachDB, BOLA commonly arises from the interaction of framework conventions, ORM mapping, and the database’s distributed SQL semantics.
Spring Data REST and Spring MVC often expose entity endpoints such as /api/users/{id} or /api/accounts/{id}. If the controller or repository does not enforce a tenant or ownership check, an attacker can increment or guess IDs and read or modify another user’s data. CockroachDB’s strong consistency and SQL semantics mean that queries like SELECT * FROM accounts WHERE id = $1 will reliably return a row if it exists, regardless of the cluster’s geo-partitioning. This reliability can give a false sense of safety: the database returns the record, but the application fails to confirm the authenticated user is authorized for that record.
A typical vulnerable pattern is a repository extending JpaRepository (or equivalent for CockroachDB via an ORM) without a tenant or user context in the query. For example:
@Repository
public interface AccountRepository extends JpaRepository {
// BOLA risk: no ownership filter
Optional findById(Long id);
}
If the service then does:
Account account = accountRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
and returns it directly to the caller without checking that the authenticated user owns this account, BOLA is present. An attacker can supply any numeric ID and access arbitrary accounts. CockroachDB’s globally consistent reads do not mitigate this; they simply ensure the query returns the latest committed value, which can still be someone else’s data.
Another vector is unsafe URL or path parameters that map to database identifiers without normalization or validation. For instance, using a UUID string as an ID is safer than integers because it prevents easy enumeration, but if the application does not enforce access control on each lookup, attackers can still iterate through valid UUIDs belonging to other users. The database will resolve each UUID to a row, and without proper authorization checks at the service or repository layer, information disclosure or unauthorized mutation occurs.
In distributed deployments, CockroachDB’s multi-region capabilities may place data across nodes, but this has no bearing on BOLA — authorization must still be enforced in the application layer. Relying on network isolation or obscurity (e.g., non-sequential IDs) is insufficient. The combination of Spring Boot’s rapid endpoint generation and CockroachDB’s reliable SQL interface makes it essential to embed ownership checks in every data access path.
Cockroachdb-Specific Remediation in Spring Boot — concrete code fixes
Remediation centers on ensuring every data access includes the authenticated user (or tenant) as a mandatory filter. Below are concrete, CockroachDB-aligned examples in Spring Boot that prevent BOLA.
1. Repository with ownership filter
Define a query that includes the user identifier. Use a custom repository implementation to keep the security boundary explicit.
@Repository
public interface AccountRepository extends JpaRepository, AccountRepositoryCustom {
// Intentionally omitted findById to avoid bypass; use findByOwnerId instead
}
public interface AccountRepositoryCustom {
Optional findByOwnerIdAndId(Principal principal, Long id);
}
@Repository
public class AccountRepositoryImpl implements AccountRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public Optional findByOwnerIdAndId(Principal principal, Long id) {
String userSub = principal.getName(); // or extract UUID/subject from auth
String sql = "SELECT a FROM Account a WHERE a.id = :id AND a.ownerId = :ownerId";
return Optional.ofNullable(em.createQuery(sql, Account.class)
.setParameter("id", id)
.setParameter("ownerId", userSub)
.getSingleResult());
}
}
2. Service with explicit ownership check
Use the custom repository method and avoid exposing raw findById. Wrap in a service that propagates the authentication context.
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
public Account getAccountForCurrentUser(Principal principal, Long id) {
return accountRepository.findByOwnerIdAndId(principal, id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
}
}
3. Controller with authenticated subject
Inject Authentication rather than relying on path variables alone. This ensures the principal is tied to the security context.
@RestController
@RequestMapping("/api/accounts")
@RequiredArgsConstructor
public class AccountController {
private final AccountService accountService;
@GetMapping("/{id}")
public ResponseEntity getAccount(@PathVariable Long id, Authentication authentication) {
Account account = accountService.getAccountForCurrentUser(authentication, id);
return ResponseEntity.ok(account);
}
}
4. Using CockroachDB-specific SQL with placeholders
When using native queries, always parameterize both the ID and the owner. CockroachDB supports prepared statements; avoid string concatenation.
@Repository
public class AccountRepositoryImpl implements AccountRepositoryCustom {
@PersistenceContext
private EntityManager em;
@Override
public Optional findByOwnerIdAndId(Principal principal, Long id) {
String userUuid = principal.getName(); // assume UUID stored as ownerId
javax.persistence.Query q = em.createNativeQuery(
"SELECT * FROM accounts WHERE id = $1 AND owner_id = $2", Account.class);
q.setParameter(1, id);
q.setParameter(2, userUuid);
return Optional.ofNullable((Account) q.getSingleResult());
}
}
5. Auditing and fallback
Consider adding an audit log entry when a forbidden attempt is detected. This does not replace authorization but helps with detection. Also ensure that error messages do not leak existence of resources; use a generic 403 rather than 404 when ownership is the suspected issue.
6. Testing the fix
Verify that a request with an authenticated user can only access records where the owner identifier matches. Use an integration test with an embedded CockroachDB instance or a test cluster to ensure the SQL filter is applied correctly.
@SpringBootTest
@AutoConfigureMockMvc
public class AccountSecurityTests {
@Autowired private MockMvc mvc;
@MockBean private AccountRepository accountRepository;
@Test
public void userCannotAccessOtherUsersAccount() throws Exception {
// arrange
Authentication auth = new TestingAuthenticationToken("user-a", null, "ROLE_USER");
SecurityContextHolder.getContext().setAuthentication(auth);
when(accountRepository.findByOwnerIdAndId(eq(auth), eq(999L))).thenReturn(Optional.empty());
// act / assert
mvc.perform(get("/api/accounts/999").with(authentication(auth)))
.andExpect(status().isForbidden());
}
}Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |