TL; DR 

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. 

Introduction

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. 

Objective

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. 

How and Where Authorization can be enforced in Java Spring

1. URL-Level Authorization 

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. 

2. Method Level Authorization 

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. 

  • It can be implemented in controller, service or repository method. 
  • Common Annotations used: 
    • @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. 

3. Object level Authorization 

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: 

  • Define Permissions-Determine specific action that needs to be controlled (e.g., update,delete,add,read,etc) 
  • Assign Permissions- Associate permission with users or roles 
  • Enforce Permissions- Use Spring Security’s mechanisms (ACLs, method security, custom evaluators) to enforce the defined permissions at the appropriate level (e.g., method call, data access). 

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. 

Attribute level Authorization 

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 

5. Role-Based Access Control (RBAC) 

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: 

  • Roles: Logical groupings of permissions (e.g., ADMIN, EDITOR, USER)  
  • Permissions: Specific actions users can perform (e.g., CREATE_POST, DELETE_USER)  
  • Role Hierarchy: Higher roles can inherit lower role permissions 

Simple role hierarchy example: 

public enum UserRole { 
    ADMIN("Can manage users and content"), 
    EDITOR("Can create and edit content"),  
    USER("Can view content"); 
} 

6. Permission-Based Authorization: Beyond Roles 

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: 

  • Specific Actions: Instead of “Is user an ADMIN?”, ask “Can user DELETE_DOCUMENT?” 
  • Flexible Combinations: Users can have custom permission sets without predefined roles 
  • Context-Aware: Permissions can be dynamic based on data ownership or business rules 
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. 

Testing Authorization in Java Spring: A Step-by-Step Secure Code Review Guide 

Step 1: Map the Application’s Authorization Model 

Goal: 

Understand authorization flow of application by checking how roles, permissions and data ownership are enforced in the code. 

What to Look For: 
  • Role Definitions – Check how roles are defined in codebase-  enums, constants or in database. 
// 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.  

  • Check if permissions are assigned based on roles. 

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 
} 

Data Flow:  

  • Trace how user identity and permissions flow from login (authentication) to business logic or service layer 
    Here’s a sample diagram illustrating a basic flow you can map during your analysis. 
Login Request (POST /login)

The user sends credentials via /login.

Authentication Filter (UsernamePasswordAuthenticationFilter)

Spring Security filters intercept the request, validate credentials, and load user details.

User DetailsService loads user by username

Spring Security filters intercept the request, validate credentials, and load user details.

Spring Security creates Authentication object (with roles/authorities)

Spring Security filters intercept the request, validate credentials, and load user details.

SecurityContextHolder stores Authentication

If successful, an Authentication object containing identity and roles is saved in SecurityContextHolder.

Controller (e.g., /api/orders)

When a secured endpoint is hit, Spring injects the current user’s identity from the context.

@PreAuthorize(“hasRole(‘ADMIN’)”) or manual check from Authentication

Access is checked using annotations like @PreAuthorize, or manually using Authentication.get Authorities).

Business Logic Layer (Service)

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: 

Red Flags: 

  • Permissions assigned based on roles, but roles are not correctly validated. 
  • Roles/permissions stored in client-side tokens without server-side validation  

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.  

  • No documentation or inconsistent role hierarchies (e.g., ADMIN inheriting USER rights)  

Step 2: Identify Access Control Enforcement Points  

Goal : 

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.  

What to Look For:  
  • Custom Authorization Logic: Check for calls to custom permission evaluators(Authentication, SecurityContextHolder, or Principal usage.)  

Security Annotations: Ensure use of annotations like @PreAuthorize, @RolesAllowed, or @Secured on controllers/services. 

  • Filters/Interceptors: Check for Filter or HandlerInterceptor implementations that enforce access control logic before request handling. 
  • Security Configurations: Review WebSecurityConfigurerAdapter (Spring Security) or web.xml (Java EE) for URL-based or endpoint-level access rules.  

Red Flags:  

  • Missing authorization checks in internal service methods (only enforced at controller level). 
  • Relying solely on client-side enforcement  
  • Lack of defence-in-depth sensitive actions allowed without deeper-layer validation – Access control on at controller level but not enforced in service.  
  • Typos or incorrect role names (e.g., hasRole('ADMINN'))  
  • No permission checks before performing actions like edit/delete/export

Step 3: Test Vertical Escalation (Role/Privilege Elevation)  

Goal : 

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. 

Code Review Focus:  

  • Role-Based Access Control (RBAC) Consistency 
    • Ensure that role checks are consistently applied across controller, service and data access layers. Look for missing or inconsistent use of annotations like @PreAuthorize, @Secured, or @RolesAllowed
  • Validate Role Escalation Paths 
    • Inspect any code that allows role changes (e.g., setRole, updateUserRole) and ensure only authorized users (e.g., admins) can invoke them. 
  • Audit Admin-Only Features 
    • Identify endpoints or methods that perform administrative actions (e.g., /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. 
  • Token/JWT Validation 
    • When roles or permissions are passed in JWT tokens, ensure they are validated server-side. Always verify the JWT signature and ensure that claims (like roles or scopes) are not trusted blindly without signature validation. Avoid using parseClaimsJwt() without checking the token’s authenticity. 
  • Ensure No Overly Permissive Endpoints: 
    • Identify endpoints marked with @PermitAll, antMatchers("**"), or missing security annotations. Ensure these endpoints do not expose sensitive functionality or provide unguarded access to critical data or actions. 
  • Role Assignment Logic 
    • Review user registration flows, account update APIs, or admin panels. Ensure that users cannot assign themselves elevated roles (e.g., through manipulated form data like role=ADMIN or isAdmin=true). Also ensure that role changes are not implicitly allowed through insecure logic. 
  • Check for Secure Default Role Assignments 
    • Ensure that newly registered users are assigned the least-privileged role by default (e.g., 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. 

Testing Tips:  

  • Verify Object Ownership Enforcement 
    Ensure that users can only access or modify their own resources. Look for explicit ownership checks in service or controller logic. 
    Example: 
@PreAuthorize("#order.ownerId == authentication.name") 
public Order getOrder(Order order) { 
    return order; 
} 
  • Audit Overly Permissive Access Rules 
    • Search for configurations like @PermitAll, antMatchers("**"), or routes missing security annotations. These can unintentionally expose sensitive endpoints to unauthenticated or unauthorized users. 
    • Review WebSecurityConfigurerAdapter or equivalent config files for catch-all patterns that allow broad access. 
  • Search for Role Manipulation Patterns 
    Use search tools (e.g., grep, IDE search) to locate potentially risky methods or flags like: 
    • setRole 
    • hasRole 
    • isAdmin 
  • setIsAdmin(true) 
    Investigate whether such logic is protected by proper role checks and ensure role changes are only performed by authorized users. 
  • Leverage SAST Tools 
    Several SAST tools such as Semgrep have predefined or community rules that help detect missing, insecure, or misused role/permission checks in Java Spring applications.  

Step 4: Test Horizontal Escalation  

Goal  

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. 

Code Review Focus:  

  • Ownership Enforcement 

Ensure that sensitive endpoints or data access layers include authorization checks to confirm that the user has access only to their own data. 

  • File Upload/Download Validation 

Ensure that uploaded/downloaded files are linked to the requesting user, and ownership is enforced both in metadata and file access logic. 

  • Unfiltered Access to Related Entities 

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

  • Parameterized Endpoints with Path Variables 
    • Check if endpoints like /api/users/{userId}/profile validate ownership  
  • Secure Query Scoping 
    • Ensure all data access queries are scoped to the user. This includes: 
    • JPQL/SQL: WHERE entity.userId = :currentUserId 
    • Criteria queries with uxser filters 
    • Repository methods that include ownership constraints 

Red Flags:  

  • No Ownership Validation on Resource Access 
    • Endpoints expose data based solely on ID (e.g., findById(id)) without confirming the resource belongs to the current user. 
  • Direct Use of User-Supplied IDs in Queries 
    • Queries like repository.findById(inputId) without validating if inputId belongs to the current user are a major risk. 
  • Generic Repository Methods Without Ownership Filters 
    • Methods like findById(), findAll() used without contextual filters can return unauthorized data if not scoped properly. 

Summary: 

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.