Broken Access Control in Spring Boot with Api Keys
Broken Access Control in Spring Boot with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when API endpoints do not properly enforce authorization checks, allowing one user to access or modify another user’s resources. In Spring Boot applications that rely on API keys for authentication, misconfigurations are common and can directly enable BOLA/IDOR and BFLA/Privilege Escalation. API keys are often treated as secrets that prove identity, but if the application does not also validate ownership or role context on each request, the key alone is insufficient to enforce fine-grained access control.
Consider a typical setup where a key is validated via a filter or interceptor, and once validated the Spring Security context is set with an authenticated principal but without sufficient authorization metadata tied to the specific resource. For example, an endpoint like /api/users/{userId}/profile might check that the API key is valid, but never confirm that the requesting user is associated with that userId. This gap maps directly to the OWASP API Top 10 Broken Object Level Authorization (BOLA) and is a frequent finding in scans run by tools such as middleBrick, which tests unauthenticated attack surfaces and correlates findings with the OWASP API Top 10 and compliance frameworks.
Spring Boot’s default security configuration does not automatically enforce object-level policies. If developers rely only on method-level @PreAuthorize with hard-coded roles and forget to bind the key to a tenant or user identifier, the attack surface expands. An attacker who obtains or guesses a valid API key can iterate over other users’ IDs and read or modify data, demonstrating BOLA/IDOR. Similarly, privilege escalation can occur when a key with broader permissions is accepted for endpoints that should require a more privileged scope, illustrating BFLA/Privilege Escalation. These issues are not theoretical; they appear in real scans where middleware validates the key but business logic skips contextual authorization checks.
Another subtle risk arises from inconsistent enforcement across endpoints. Some paths may use @Secured or @PreAuthorize with role expressions, while others rely on custom annotations or no annotations at all. If API key validation is performed early and the resulting authentication object lacks granular authorities, the application may inadvertently permit unauthenticated-appearing but actually unauthorized access. This inconsistency shows up in property authorization checks where certain data fields are exposed based on endpoint rather than on user-context. middleBrick’s parallel checks for Authentication, Property Authorization, and BOLA/IDOR are designed to surface these discrepancies by comparing spec-defined security schemes with runtime behavior, including OpenAPI/Swagger 2.0/3.0/3.1 documents with full $ref resolution.
Supply chain and inventory management concerns also intersect with broken access control. If API keys are issued without scoping to specific resource sets or without revocation mechanisms, compromised keys persist in the environment and enable long-term unauthorized access. In addition, unsafe consumption of user-supplied identifiers without canonicalization or strict type checks can allow IDOR via path manipulation, such as ../ sequences or ambiguous parameter binding. These patterns are precisely what middleBrick tests through its 12 security checks, which run in parallel to deliver a security risk score and prioritized findings with remediation guidance.
Api Keys-Specific Remediation in Spring Boot — concrete code fixes
To mitigate Broken Access Control when using API keys in Spring Boot, you must couple authentication with explicit authorization checks that include the resource owner context. Below are concrete, syntactically correct code examples that demonstrate a secure approach.
1. Key validation with user binding and path ownership check
Store API keys in a secure repository mapped to user identifiers and scopes. In your filter or interceptor, after validating the key, set an authentication that includes authorities and the user ID, then enforce ownership in the handler.
@Component
public class ApiKeyValidationFilter extends OncePerRequestFilter {
private final ApiKeyService apiKeyService; // Your service that loads key metadata
public ApiKeyValidationFilter(ApiKeyService apiKeyService) {
this.apiKeyService = apiKeyService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String key = extractKey(request);
if (key != null) {
ApiKeyMetadata metadata = apiKeyService.lookupByKey(key);
if (metadata != null && metadata.isActive()) {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
metadata.getUserId(),
null,
AuthorityUtils.createAuthorityList(metadata.getScope())
);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
private String extractKey(HttpServletRequest request) {
String header = request.getHeader("X-API-Key");
if (header != null && !header.isBlank()) {
return header;
}
return request.getParameter("api_key");
}
}
2. Controller with path variable ownership verification
In your controller, do not rely on the authentication principal name alone; explicitly verify that the requested resource belongs to the authenticated user.
@RestController
@RequestMapping("/api/users")
public class UserProfileController {
private final UserRepository userRepository; // Your domain repository
public UserProfileController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/{userId}/profile")
public ResponseEntity<UserProfileDto> getProfile(@PathVariable Long userId, Authentication authentication) {
String authenticatedUserId = authentication.getName(); // or extract from principal
if (!authenticatedUserId.equals(userId.toString())) {
throw new AccessDeniedException("Cannot access another user’s profile");
}
return userRepository.findById(userId)
.map(UserProfileMapper::toDto)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
3. Method-level security with expression-based ownership
Use Spring Security’s expression-based authorization to enforce ownership at the method level. This complements web layer checks and provides defense-in-depth.
@Service
public class ProfileService {
private final UserRepository userRepository;
public ProfileService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@PreAuthorize("@profileOwnershipChecker.isOwner(#userId, authentication)")
public UserProfileDto getProfileByUserId(Long userId) {
return userRepository.findById(userId)
.map(UserProfileMapper::toDto)
.orElseThrow(() -> new EntityNotFoundException("Profile not found"));
}
}
@Component
public class ProfileOwnershipChecker {
public boolean isOwner(String resourceUserId, Authentication authentication) {
String authenticatedId = authentication.getName();
return resourceUserId.equals(authenticatedId);
}
}
4. Enforce scope and privilege mapping at the key level
Ensure each API key is associated with a defined scope and that endpoints validate that scope before performing sensitive actions. This helps mitigate privilege escalation by ensuring that a key with read-only scope cannot invoke write endpoints.
@Component
public class ScopeValidationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getAuthorities().stream().anyMatch(g -> g.getAuthority().equals("SCOPE_write"))) {
// Allow mutation endpoints
filterChain.doFilter(request, response);
return;
}
String path = request.getRequestURI();
if (path.startsWith("/api/admin") || path.startsWith("/api/write")) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Insufficient scope");
return;
}
filterChain.doFilter(request, response);
}
}
These examples emphasize that API key authentication must be paired with explicit resource ownership checks and scope-aware authorization. By combining filter-based authentication with controller-level and method-level security, you reduce the risk of BOLA/IDOR and BFLA/Privilege Escalation. middleBrick can help validate such implementations by scanning for misconfigurations and mapping findings to standards like OWASP API Top 10 and compliance frameworks, offering prioritized remediation guidance.