Bola Idor in Spring Boot with Api Keys
Bola Idor in Spring Boot with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API exposes object identifiers (IDs) and fails to enforce that the requesting user is authorized to access that specific object. In a Spring Boot application that uses API keys for authentication, BOLA can arise when keys are treated as a global authentication token but the application still relies on predictable object IDs (database primary keys, UUIDs, or slugs) to locate resources without verifying ownership or tenant boundaries.
Consider a typical setup where an API key identifies an integration or a consumer, and endpoints such as /api/v1/organizations/{orgId}/projects/{projectId} are implemented with a repository find-by-ID call. If the service method does not include the API key’s associated scope (for example, the list of organization IDs the key is allowed to access), an attacker can take a valid project ID observed from another request or from enumeration and issue requests with a legitimate API key that has broad access. The key grants access to the API, but the application does not enforce that the key’s scope matches the requested project ID, resulting in unauthorized data access.
Spring Boot applications often map API keys to an entity like ApiKey and lazily load associated metadata or allowed scopes. A vulnerability emerges when the controller or service layer does not re-check those scopes against the resource being accessed. For example, using findById(id) without ensuring the returned entity’s organization matches the authenticated key’s allowed organizations is a classic BOLA pattern. Attackers do not need to compromise the key; they simply manipulate IDs in URLs or parameters to traverse relationships (e.g., cycling through project IDs, user IDs, or invoice IDs).
Another common scenario involves path or query parameters that expose internal identifiers. If your endpoint exposes /api/v1/users/{userId}/settings and resolves the user by ID alone, an attacker can change userId to view or modify settings of other users, provided the API key is valid and does not enforce user-level boundaries. Even with API keys, Spring Data JPA repositories that use derived queries without a tenant or scope filter enable BOLA. The root cause is a lack of binding between the key’s permissions and the object ownership check at the data-access layer.
In summary, using API keys in Spring Boot does not automatically prevent BOLA. The vulnerability appears when authorization logic treats the key as proof of access to any resource identified by a user-supplied ID, rather than validating that the key’s associated permissions or tenant context includes the targeted object. Without explicit scope checks, predictable IDs become an invitation for unauthorized traversal across the API surface.
Api Keys-Specific Remediation in Spring Boot — concrete code fixes
To mitigate BOLA when using API keys in Spring Boot, you must bind each API key to a scope (such as organization IDs or allowed project IDs) and enforce that scope at every data-access operation. The remediation focuses on moving from key-only authentication to key-plus-scope authorization, using Spring Security and repository-layer checks.
1. Model your API key with scope
Define entities that capture the relationship between an API key and the resources it can access. For example, an API key may be valid only for certain organizations:
@Entity
public class ApiKey {
@Id
private String key;
private String ownerName;
@ElementCollection
private Set<UUID> allowedOrganizationIds = new HashSet<>();
// standard getters/setters
}
@Entity
public class Project {
@Id
private UUID id;
private String name;
@ManyToOne
private Organization organization;
// standard getters/setters
}
2. Load the key and scope in a custom authentication provider
Use a filter or OncePerRequestFilter to extract the API key and build an Authentication object that includes authorities and the set of allowed organization IDs:
@Component
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private ApiKeyRepository apiKeyRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null) {
ApiKey key = apiKeyRepository.findByKey(apiKey)
.orElseThrow(() -> new BadCredentialsException("Invalid API key"));
List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("SCOPE_read"));
Authentication auth = new ApiKeyAuthentication(key, authorities, key.getAllowedOrganizationIds());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
public static class ApiKeyAuthentication implements Authentication {
private final ApiKey key;
private final Collection<? extends GrantedAuthority> authorities;
private final Set<UUID> allowedOrganizationIds;
private boolean authenticated = true;
public ApiKeyAuthentication(ApiKey key, Collection<? extends GrantedAuthority> authorities, Set<UUID> allowedOrganizationIds) {
this.key = key;
this.authorities = authorities;
this.allowedOrganizationIds = allowedOrganizationIds;
}
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
@Override public Object getCredentials() { return key; }
@Override public Object getDetails() { return null; }
@Override public Object getPrincipal() { return key.getOwnerName(); }
@Override public boolean isAuthenticated() { return authenticated; }
@Override public void setAuthenticated(boolean isAuthenticated) { this.authenticated = isAuthenticated; }
@Override public String getName() { return key.getKey(); }
public Set<UUID> getAllowedOrganizationIds() { return allowedOrganizationIds; }
}
3. Enforce scope in service/repository layer
Inject the authentication into your service and validate scope before querying by ID:
@Service
public class ProjectService {
@Autowired
private ProjectRepository projectRepository;
public Project getProject(UUID projectId, Authentication auth) {
ApiKeyAuthentication keyAuth = (ApiKeyAuthentication) auth;
return projectRepository.findByIdAndAllowedOrganizationIds(projectId, keyAuth.getAllowedOrganizationIds())
.orElseThrow(() -> new AccessDeniedException("Access denied"));
}
}
@Repository
public interface ProjectRepository extends JpaRepository<Project, UUID> {
@Query("SELECT p FROM Project p WHERE p.id = :id AND p.organization.id IN :allowedOrganizationIds")
Optional<Project> findByIdAndAllowedOrganizationIds(@Param("id") UUID id, @Param("allowedOrganizationIds") Set<UUID> allowedOrganizationIds);
}
4. Use Spring Expression-Based Security for method-level checks
With a custom permission evaluator, you can centralize BOLA checks across controllers:
@Component
public class ApiKeySecurityExpressionRoot {
public boolean hasProjectAccess(UUID projectId, ApiKeyAuthentication auth) {
return auth.getAllowedOrganizationIds().contains(projectId);
}
}
// In a controller
@PreAuthorize("@apiKeySecurity.hasProjectAccess(#projectId, authentication)")
@GetMapping("/projects/{projectId}")
public ResponseEntity<Project> getProject(@PathVariable UUID projectId) {
Project project = projectService.findById(projectId);
return ResponseEntity.ok(project);
}
These steps ensure that a valid API key is not sufficient on its own; the key’s associated scope is checked against the object being accessed, effectively neutralizing BOLA in a Spring Boot API that uses API keys.
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 |