Login Request (POST /login)
The user sends credentials via /login.
Privilege escalation vulnerabilities, often caused by broken or missing authorization, can slip past dynamic tests, like pentests, due to time constraints or limited coverage. This blog dives into how secure code review can fill those gaps, especially in Java Spring applications. We explore how to identify insecure patterns and misconfigurations in Spring’s built-in access control features – such as annotations, expressions, and filters to detect privilege escalation paths early in the SDLC.
Testing for authorization issues is both essential and challenging. Authorization flaws such as Broken Access Control, which ranks #1 on the OWASP Top 10 can lead to serious consequences, including privilege escalation, unauthorized access to sensitive data, and system manipulation. These high-impact vulnerabilities are often hard to detect in complex applications, as identifying them requires a deep understanding of user roles, application workflows, and resource ownership patterns.
Secure Code Review (SCR) is essential for identifying authorization vulnerabilities by examining application access control logic directly in the source code. By analyzing code at rest, SCR enables systematic examination of roles, permissions, and security rules, detecting inconsistencies, edge cases, and business logic misalignments early in development. This proactive approach strengthens authorization controls and prevents privilege escalation before deployment.
This blog targets security professionals, software developers, and code reviewers who are involved in securing Java Spring applications. It aims to help readers identify and mitigate authorization flaws during code reviews. Readers will learn to recognize common authorization issues, spot insecure access control patterns, and discover insecure patterns to prevent privilege escalation attacks particularly in Java Spring. While focused on Java development, the principles and patterns discussed also apply broadly to other programming languages and frameworks
Before diving into testing for authorization flaws, it’s essential to first understand how Spring authorization controls are typically implemented. The following sections explore common authorization implementation patterns in Spring 6 onwards, to build the foundation for recognizing potential security flaws.
URL-level authorization controls access to specific endpoints based on user roles. In Spring Security, this is configured by defining request matchers that map URL patterns to required authorities. The example below shows how to restrict different application sections to specific roles:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/homepage/**").hasAnyRole("USER", "ADMIN") .requestMatchers("/userPage/**").hasRole("USER") .requestMatchers("/adminPage/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/loginPage") .defaultSuccessUrl("/homePage") ); return http.build(); } }
In this configuration, /homepage/**
is accessible to both USER and ADMIN roles, while /userPage/**
and /adminPage/**
are restricted to their respective roles. The anyRequest().authenticated()
ensures all other endpoints require authentication. Spring Security evaluates these rules in order, so more specific patterns should come first.
The @PreAuthorize
annotation blocks unauthorized users before method execution – only ADMINs can delete users. The @PostAuthorize
annotation evaluates after the method runs, allowing users to retrieve documents they created or letting ADMINs access any document. This demonstrates how method-level security can enforce both role-based and data-ownership authorization patterns.Method-level authorization in Spring Security enables fine-grained control over access to specific methods within an application. It allows developers to define security constraints directly on methods, ensuring that only authorized users can execute them.
@PreAuthorize("hasRole('ADMIN')")
@PostAuthorize("returnObject.owner == authentication.name")
@Secured("ROLE_ADMIN")
@RolesAllowed("ROLE_ADMIN")
@PreAuthorize("hasRole('ADMIN')") public void deleteUser(Long userId) { userRepository.deleteById(userId); } @PostAuthorize("returnObject.createdBy == authentication.name or hasRole('ADMIN')") public Document getDocument(Long documentId) { return documentRepository.findById(documentId); }
The @PreAuthorize
annotation blocks unauthorized users before method execution – only ADMINs can delete users. The @PostAuthorize
annotation evaluates after the method runs, allowing users to retrieve documents they created or letting ADMINs access any document. This demonstrates how method-level security can enforce both role-based and data-ownership authorization patterns.
Object-level authorization controls access to individual records, not just categories of data. For example, a user might be able to edit their own documents but not others’ documents. This is crucial for scenarios where different users should have different levels of access to individual records.
Spring Security provides several mechanisms to implement object-level authorization:
Example:
Object-level authorization is implemented using Spring Security’s hasPermission()
expression. Here’s how to secure a method that requires write permission on a specific document:
@PreAuthorize("hasPermission(#document, 'write')") public Document writeDocument(Document document) { return document; } public class CustomPermissionEvaluator implements PermissionEvaluator { public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { // Custom logic here } }
The @PreAuthorize("hasPermission(#document, 'write')")
annotation delegates to hasPermission()
in your custom PermissionEvaluator, where you define the logic to check if the authenticated user has 'write'
access to the given document. This enables fine-grained, object-level authorization based on ownership, sharing, or roles.
Think of attribute-level authorization like a privacy filter. Instead of hiding an entire profile, you can just hide the sensitive parts – like showing someone’s name but masking their contact details based on who’s asking.
This is implemented using DTOs (Data Transfer Objects) that include or exclude fields based on the user’s permissions.
Example:
Here’s an example where contact information is only visible to administrators:
public class UserDTO { private String username; private int contact_no; public UserDTO(User user, Authentication auth) { this.username = user.getUsername(); if (auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) { this.contact_no = user.getContactNo(); } else { this.contact_no = null; } } }
This DTO constructor dynamically builds the response based on the user’s role. All users receive the username
field, but only administrators get the contact_no
field. This approach ensures sensitive data is filtered at the application layer before being sent to the client, providing field-level security without complex database queries or multiple API endpoints
The URL-level, method-level, and object level authorization examples we’ve covered above all follow a common pattern: Role-Based Access Control (RBAC). RBAC serves as the organizing principle that determines who can access what in your application.
RBAC organizes permissions around roles rather than individual users. Instead of assigning permissions directly to each user, you create roles (like ADMIN, EDITOR, USER) and assign permissions to those roles. Users then inherit permissions through their assigned roles.
Key RBAC Components:
Simple role hierarchy example:
public enum UserRole { ADMIN("Can manage users and content"), EDITOR("Can create and edit content"), USER("Can view content"); }
While RBAC groups permissions into roles, Permission-Based Access Control (PBAC) provides even finer control by focusing on specific actions rather than broad role categories. This approach is particularly useful when you need granular control that doesn’t fit neatly into predefined roles.
Key PBAC Characteristics:
public enum Permission { CREATE_DOCUMENT, EDIT_DOCUMENT, DELETE_DOCUMENT, MANAGE_USERS } @PreAuthorize("hasAuthority('EDIT_DOCUMENT')") public Document editDocument(Long id) { return documentService.update(id); }
Having explored authorization implementation patterns in Spring, we can now focus on practical vulnerability detection in source code. The section below provides a step-by-step approach to testing authorization issues in Java Spring applications, with specific examples and red flags that indicate potential security vulnerabilities.
Understand authorization flow of application by checking how roles, permissions and data ownership are enforced in the code.
// Roles defined as Java enums in com.example.security.Role Public enum Role { ADMIN, USER, GUEST }
Role Assignments: Determine how users inherit role. Check if role assignments are dynamic or hardcoded.
// Hardcoded (even with database) user.setRole(Role.Admin);
// Dynamic user.setRole(roleNameFromExternalSource);
Dynamic role assignment offers more flexibility by allowing roles to be managed externally without requiring code changes.
Often, roles are used to assign permissions, and access checks are done via permissions.
Map<String, List<String>> rolePermissions = Map.of( "ROLE_ADMIN", List.of("READ", "WRITE"), "ROLE_USER", List.of("READ") ); List<String> permissions = rolePermissions.get(user.getRole());
Permissions are determined by the user’s role, promoting centralized role-based access control instead of assigning permissions directly to each user.
Check if assigned permissions are correctly validated
Example:
if (currentUser.getPermissions().contains("WRITE")) { // allow write operation }
The user sends credentials via /login.
Spring Security filters intercept the request, validate credentials, and load user details.
Spring Security filters intercept the request, validate credentials, and load user details.
Spring Security filters intercept the request, validate credentials, and load user details.
If successful, an Authentication object containing identity and roles is saved in SecurityContextHolder.
When a secured endpoint is hit, Spring injects the current user’s identity from the context.
Access is checked using annotations like @PreAuthorize, or manually using Authentication.get Authorities).
If authorized, execution flows into the service/business logic
As you analyze the application’s authorization model, keep an eye out for misconfigurations or patterns that weaken access control. Here are some red flags to watch for:
Example:
Claims claims = Jwts.parserBuilder() .build() .parseClaimsJwt(token) // ❌ Parses token without verifying signature .getBody(); List<String> roles = claims.get("roles", List.class);
This code parses the JWT but does not validate the signature. Without verifying the token’s integrity, a malicious user can modify roles or permissions in the token, leading to privilege escalation or impersonation.
Locate where the application checks permissions before allowing access to sensitive actions or data. These checks enforce authorization rules, ensuring users can only do what they are allowed to do.
Authentication
, SecurityContextHolder
, or Principal usage.) Security Annotations: Ensure use of annotations like @PreAuthorize
, @RolesAllowed
, or @Secured
on controllers/services.
Filter
or HandlerInterceptor
implementations that enforce access control logic before request handling. WebSecurityConfigurerAdapter
(Spring Security) or web.xml (Java EE) for URL-based or endpoint-level access rules. hasRole('ADMINN')
) edit/delete/export
. Ensure that users with lower privileges (e.g., regular users) cannot gain access to higher-privileged functionality or data, such as administrative features or sensitive system operations.
@PreAuthorize
, @Secured
, or @RolesAllowed
. setRole
, updateUserRole
) and ensure only authorized users (e.g., admins) can invoke them. /admin
, /manageUsers
, /config
, or database-level controls). Confirm they are properly protected by role checks such as @PreAuthorize("hasRole('ADMIN')")
or equivalent configuration-based access rules. parseClaimsJwt()
without checking the token’s authenticity. @PermitAll
, antMatchers("**")
, or missing security annotations. Ensure these endpoints do not expose sensitive functionality or provide unguarded access to critical data or actions. role=ADMIN
or isAdmin=true
). Also ensure that role changes are not implicitly allowed through insecure logic. ROLE_USER
). Avoid defaulting to elevated roles (ROLE_ADMIN
, ROLE_MANAGER
) or leaving roles unassigned, which may lead to unintended access depending on fallback behaviour. @PreAuthorize("#order.ownerId == authentication.name") public Order getOrder(Order order) { return order; }
@PermitAll
, antMatchers("**")
, or routes missing security annotations. These can unintentionally expose sensitive endpoints to unauthenticated or unauthorized users. WebSecurityConfigurerAdapter
or equivalent config files for catch-all patterns that allow broad access. setRole
hasRole
isAdmin
setIsAdmin(true)
Ensure that users can only access or manipulate resources they own, even if they share the same role. Users must not be able to view, edit, or delete another user’s data by guessing identifiers or manipulating requests.
Ensure that sensitive endpoints or data access layers include authorization checks to confirm that the user has access only to their own data.
Ensure that uploaded/downloaded files are linked to the requesting user, and ownership is enforced both in metadata and file access logic.
For relational models (e.g., User → Cart or Orders), verify that users cannot access another user’s entities by altering path variables, query parameters, or request bodies.
GET /api/cart/12345 ←
No ownership check; allows access to someone else’s cart
/api/users/{userId}/profile
validate ownership WHERE entity.userId = :currentUserId
findById(id)
) without confirming the resource belongs to the current user. repository.findById(inputId)
without validating if inputId
belongs to the current user are a major risk. findById()
, findAll()
used without contextual filters can return unauthorized data if not scoped properly. In this blog, we explored how authorization can be implemented in Java Spring and how to review and test authorization mechanisms during a Secure Code Review (SCR). We walked through the authorization model, identified access control enforcement points, and examined how to detect both vertical and horizontal privilege escalation issues. By breaking down complex access control concepts into actionable review steps, this guide aims to help security testers assess authorization logic. I hope it serves as a practical reference for anyone involved in secure code review of Spring application.
At NetSPI, our Secure Code Review services identify broken or inconsistent authorization logic by analyzing how roles and permissions are enforced within the codebase. Complementing this, our penetration testing service dynamically verifies authorization flows in a running application to uncover privilege escalation vulnerabilities. Combining these into a source code–assisted penetration testing approach significantly increases coverage, especially when testing applications within tight, time-boxed engagements.
NetSPI discovered a remote code execution vulnerability in SailPoint IQService using default encryption keys. Exploit details, discovery methods, and remediation guidance included.
Organizations face threats beyond their perimeter. Explore how dark web monitoring, breach data tracking, and public exposure detection strengthen your EASM strategy.
Learn how Azure Load Testing's JMeter JMX and Locust support enables code execution, metadata queries, reverse shells, and Key Vault secret extraction vulnerabilities.