TL;DR
Spring is a web application framework ecosystem that runs on the JVM. The first release was in 2003 under the Apache 2.0 License with Spring 1.0, introduced as an alternative to address the complexity and productivity issues of J2EE/EJB at the time. Since then, Spring has rapidly grown with numerous releases and additional technologies, and in particular, it has become one of the most widely used web frameworks in South Korea, with many companies adopting it.

2025 StackOverFlow Web Frameworks Survey
Spring provides a range of built-in security technologies. At its core is Spring Security, which supports authentication/authorization control, XSS defense, CSRF protection, and various security policies that make it convenient for companies to adopt. As a result, from an attacker’s perspective, targeting Spring-based services is relatively difficult. Compared to other frameworks, there are fewer attack vectors, and vulnerabilities are harder to find. Through this research, Rewrite aims to examine how Spring operates, investigate and analyze recent CVEs, review existing attack scenarios, and identify potential new attack vectors.
Spring vs Spring boot vs Spring Security
Spring
Background
Spring is a lightweight IoC container and a comprehensive application framework for Java/Kotlin applications. It builds applications based on POJOs (Plain Old Java Objects) and eliminates unnecessary complexity in code, reducing overall code complexity. As an open-source framework, its purpose is to lower coupling between objects and ensure maintainability and testability through dependency injection and AOP (Aspect-Oriented Programming). The following chapter explains the components of Spring.
Components and Features
The Spring framework is composed of around 20 modules. When organized in a tree structure, these modules are arranged as follows.
1 | Spring Framework |

Diagram of the Main Module Hierarchy
Core Container(Core Spring)
The Core Container consists of the Core, Beans, Context, and Expression Language modules.
Core, Beans
- Provide the core functionalities of the framework, such as IoC and DI
- IoC (Inversion of Control): The inversion of the control of object lifecycles
- DI (Dependency Injection): Injecting dependencies (such as objects or classes) into a component (Class-in-Class)
Context
Building on the solid foundation provided by the Core and Beans modules, the framework offers a consistent API that allows easy retrieval of objects. This approach is similar to the JNDI registry mechanism.
1 | DataSource ds = (DataSource) ctx.lookup("java:/comp/env/jdbc/MyDB"); // Original JNDI |
Spring ApplicationContext
1 | ApplicationContext ctx = …; |
Expression Language
SpEL (Spring Expression Language) is the expression module that supports exploring and manipulating object graphs at runtime, setting and retrieving property values, performing property assignments, providing context support, and looking up objects by name within the Spring IoC container.
Data Access / Integration
The data access/integration layer consists of the JDBC, ORM, OXM, JMS, and Transaction modules.
JDBC Abstraction Layer
- Provides abstractions such as
JdbcTemplate, implementing the template method pattern to reduce repetitive code - Converts DBMS-specific error codes into common exception classes, allowing consistent error handling
ORM
- Provides an integration layer for object-relational mapping APIs
OXM
- Offers an abstraction layer supporting object/XML mapping implementations
JMS
- Provides functionality for creating and consuming messages
Transaction Module
- Delivers a consistent abstraction for both programmatic and declarative transaction management across special interface implementations and all POJOs
- Supports the
DataAccessExceptionhierarchy and transaction synchronization storage with JCA functionality
AOP / Instrumentation
AOP
- Spring provides rich support for aspect-oriented programming (AOP) through the AOP module, helping to reduce coupling between objects
- Achieved by separating logic using method interceptors and pointcuts
Instrumentation
- Provides the ability to add agents to the JVM
- Includes a weaving agent for Tomcat, which transforms class files as they are loaded by the Tomcat class loader
Web(MVC / Remoting)
Web
- Provides basic web integration features such as multipart file upload, Servlet Listener, and IoC container initialization using a web-oriented application context
Web Servlet
- Includes Spring’s implementation of MVC
Web Struts
- Provides support classes for integrating the classic Struts web layer with Spring (support discontinued from version 3.0)
Web Portlet
- Offers an MVC implementation for the Portlet environment
- Reflects the functionality of the Web Servlet module
POJO (Plain Old Java Objects)
POJO itself is not a framework feature, but it can be considered the core of the development model that Spring pursues. One of Spring’s most important characteristics is enabling POJO-based programming, which can even be regarded as its foundation.
POJO
- Refers to plain Java objects with little to no special conventions, inheritance, or annotations
- Typically implemented as a
public classin the form of a regular class - IoC/DI, AOP, and PSA are provided to support POJO-based programming

IoC(Inversion of Control)
- A design principle in which the framework controls the application flow, allowing developers to focus solely on business logic
1 | Service s = new Service(new Dao()); |
Normally, objects would have to be created directly using new as shown above.
1 |
|
By applying IoC as shown above, dependencies can be declared only through the constructor. In other words, IoC means that Spring, not the developer, creates the objects required by a specific class and establishes their dependencies.

Spring IoC container
DI (Dependency Injection)
- A design technique where an object’s dependencies are not created directly but are injected externally, as shown above
- DI is the mechanism used to implement the IoC principle
AOP (Aspect-Oriented Programming)
- Separates core logic from cross-cutting concerns and injects them automatically at desired points at runtime using proxies or bytecode manipulation
- Cross-cutting functionalities are separated into distinct objects (Aspects)
- Developers can write clean core logic, while shared logic is centrally managed in the separated Aspect
PSA (Portable Service Abstraction)
- In Spring, switching from one database to another can be done while keeping the usage method the same
- This is possible because Spring provides abstract service interfaces, commonly referred to as JDBC
- Database vendors implement their code based on JDBC, and this abstraction of services for consistent use is called PSA
Spring Boot
Background
Spring Boot provides an environment where applications can be run instantly through features such as auto-configuration and an embedded launcher (e.g., Tomcat). In contrast, the traditional Spring Framework required developers to manually configure the application context, servlet settings, and dependency management, which created a high entry barrier for initial development. Spring Boot removes this barrier and offers a variety of developer conveniences to make setting up the Spring environment easier. In essence, Spring Boot can be seen as a separate framework that simplifies the necessary configuration when building applications with Spring.
Components and Features
- Auto-configuration: Most configurations are automatically applied without explicit developer setup
- Starter dependencies: Bundled dependency packages designed for specific purposes
- Embedded Tomcat: Provides embedded Tomcat or Jetty, enabling standalone execution without WAR deployment
- Production-ready Actuator: Includes operational tools such as health checks and log viewing
- Quick configuration: Supports fast setup using
application.ymlorapplication.properties
Spring Security
Background
Spring Security is a security framework that adds authentication and authorization capabilities to Spring applications. It provides built-in patterns for implementing authentication/authorization and supports various authentication mechanisms such as session-based login, OAuth2, and JWT. It also delivers core features necessary for web application security, including URL access control, method-level authorization checks, CSRF protection, and security header configuration. Because of these characteristics, Spring Security is almost always adopted to enhance the security of Spring applications. However, vulnerabilities can sometimes arise within the very framework introduced for security, which will be discussed in the CVE chapter.
Components and Features
- Authentication / Authorization: Manages user authentication and access control
- Filter: Similar to middleware; servlet filters intercept the request/response flow; operates at the servlet container level and executes sequentially via a FilterChain
- SecurityContextHolder: Stores the security context of the current user
- PasswordEncoder: Handles password encryption, e.g., Bcrypt
- UserDetails / UserDetailsService: Interfaces for retrieving user information
- CSRF, CORS, Session: Provides built-in support for standard web security settings
Additionally, Spring Security is built on four core concepts:
- Authentication
- Authorization
- Password storage
- Servlet filter
To use Spring Security, you need to add the following dependency to your pom.xml (for Maven).
1 | <properties> |
1 | <dependency> |
The following is a diagram of the Spring Security architecture.

The following is a diagram of the internal structure of Spring Security.

Spring Build
Spring can be built easily using various build tools. During application development, it is often necessary to download and manage many external libraries. By using build tools, developers only need to specify the type and version of each library, and the tool automatically downloads and manages them (similar to Python’s pip). The most commonly used tools for this purpose are Maven and Gradle. This chapter introduces both tools and explains their respective build processes.
Maven
Maven is a traditional build and project management tool created by Apache, based on declarative XML (pom.xml).
Its long-established ecosystem provides many plugins and references, which is an advantage. Although XML-based configuration can be verbose, Maven is still widely used in large-scale enterprise projects. Below is an example of a pom.xml file.
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
Maven allows building and running with the following commands:
1 | mvn clean package -DskipTests # Build while skipping tests |
Gradle
Gradle is a build tool based on Groovy/Kotlin DSL (Domain-Specific Language). By using DSL syntax, it is much more concise than Maven, which relies on XML, and it offers greater performance optimization and flexibility. In recent years, Gradle has become the default choice for Spring Boot projects, with advantages such as faster build speeds through caching and incremental builds.
1 | plugins { |
The build and installation steps for Gradle are as follows.
1 | ./gradlew build |
Spring Build Tool Options (feat. Gradle)
Spring build tools provide various configuration options. Gradle, in particular, offers several dependency-related options, with the most common being as follows:
Types of ClassPath
compileClasspath
- Includes all class files and libraries needed to compile project source code
- Used only for reference during code writing/compilation; may not be included at runtime
runtimeClasspath
- Includes all class files and libraries needed to run the project
- These are the libraries the JVM must actually load during execution
In general, most dependencies included in runtimeClasspath are also present in compileClasspath.
testCompileClasspath
- Dependencies required for compiling test code
testRuntimeClasspath
- Dependencies required for running tests
Dependency Options
implementation
- Libraries required for both compilation and runtime
- Used at build time for compilation and also included in the build output
- Not exposed to other modules that depend on this module (internal)
1 | dependencies { |
api
- Dependencies that are part of the public API
- Exposed to other modules depending on this module (public)
- If the dependency library changes, modules depending on this module must also be rebuilt
1 | // :core module |
runtimeOnly
- Libraries needed only at runtime (not added to the classpath at build time)
- No need for them during compilation
- Example: JDBC drivers
1 | dependencies { |
compileOnly
- Required only at compile time, not needed at runtime
- Used for compilation but excluded from the build output
1 | dependencies { |
Build Process
The command to build and run a Spring application is as follows:
1 | ./mvnw spring-boot:run |
Below is the output of executing the command.
1 | . ____ _ __ _ _ |
./mvnw- Automatically downloads and runs the Maven version specified in the project → prevents version differences between team members
- Executes the
rungoal of thespring-boot-maven-plugin- Other goals include
repackage,stop, etc. (each with different logic), whilerunis responsible for compiling and running the source code
- Other goals include
Build Process
- Source code compilation & resource copying
- Executes the
compilephase → generatestarget/classes
- Executes the
- Classpath assembly
- Combines
target/classeswith dependencies (jars fetched by Maven) → builds the runtimeclasspath
- Combines
- Main class discovery
- Identifies the class annotated with
@SpringBootApplication(e.g.,DemoApplication) as the entry point - Alternatively,
start-classcan be specified inpom.xml
- Identifies the class annotated with
- JVM process execution
- A boot launcher such as
org.springframework.boot.devtools.restart.RestartLauncherruns themain()method - DevTools: supports automatic restart when code changes are detected
- A boot launcher such as
- SpringApplication.run() invocation
- Initializes the IoC container, scans Beans, injects dependencies (DI), and starts the embedded Tomcat/Jetty/Undertow server
Spring Optimization
Spring leverages tools such as Gradle and Maven to improve build optimization. For example, starting with Spring Boot 3, GraalVM Native Image is supported. Unlike traditional Spring applications packaged as JAR/WAR and running on the JVM, the Native Image approach uses GraalVM’s AOT (Ahead Of Time) compiler to precompile machine code binaries. This produces standalone executables that can run without the JVM. As a result, Spring applications can be deployed as lightweight containers that start within tens of milliseconds (around 50× faster than the JVM).
GraalVM vs JVM
Build time
- JVM: build in a few seconds
- GraalVM Native Image: requires several minutes since all code is precompiled to machine code
Metadata
- JVM: naturally handles reflection, proxies, and dynamic class loading at runtime
- GraalVM Native Image: statically compiled; dynamic features require explicit
metadata - Spring can auto-infer much of this, but external libraries may need manual metadata configuration
Classpath and Bean conditions fixed
- In a GraalVM native build, the classpath and Bean conditions are fixed at build time
- Runtime changes such as DB URL or passwords are allowed, but switching DB types or altering the structure of Spring Beans is not possible
Understanding Spring Components
This chapter covers functions and components used in Spring, along with debugging methods to analyze their code-level behavior. To understand Spring’s behavior at the code level, the build process and debugging approaches were studied and are described here.
How to Debug Spring Applications
Debugging will be demonstrated using Visual Studio Code.
- Install VS Code extensions
- Extension Pack for Java
- Spring Boot Extension Pack
- Navigate to the start-class file of the application and press
Command + Shift + D- Click
create a launch.json
- Click
Write the configuration as follows:
1 | { |
- set breakpoint

4. Run and Debug selection in VS Code
- Attach mode: Attaches directly to an application already running via Maven
- A specific port is set for this reason
- Launch mode: Runs and debugs the Spring app directly from VS Code
5. Inspecting call stack and threads after connecting to the app
Threads
- Threads currently running inside the JVM
Reference Handler,Finalizer,Signal Dispatcher: internal Java runtime management threadsCatalina-utility,http-nio-9898-exec-N: worker threads in Tomcat that process requestscontainer-0: main service thread for Spring/Tomcat
Call Stack
- Traces the methods invoked up to the current breakpoint
- Example sequence:
ApplicationFilterChain.doFilter→ApplicationFilterChain.internalDoFilter→HttpServlet.service→ … →NativeMethodAccessorImpl.invoke0→HelloController.home()
Using this approach, debugging Spring applications becomes straightforward. Additionally, it is also possible to build a debugging environment in the CLI using JDB (Java Debugger). Rewrite’s researchers used these methods to debug Spring apps and analyze the behavior of various components.
Spring Annotations
Spring annotations are metadata markers designed to simplify repetitive configurations and code in the application development process. They can be applied to classes, methods, and fields to declaratively specify behaviors or settings. Common examples include @Component, @Service, @Repository, and @Transactional. These annotations replace XML-based configuration, improve code readability and maintainability, and allow developers to focus on business logic. Below are the main categories:
Stereotypes (Bean Registration)
@Component→ Registers a generic Bean@Service→ Registers a service-layer Bean@Repository→ Registers a DAO/Repository Bean (includes exception translation)@Controller→ Registers an MVC controller@RestController→ REST API controller (@Controller + @ResponseBody)@Configuration→ Registers configuration Beans
Dependency Injection (DI)
@Autowired→ Type-based automatic injection@Qualifier→ Injects a specific Bean by name@Resource→ JSR-250 injection (name-based priority)@Value→ Injects property values
Spring Boot Specific
@SpringBootApplication→ Combination of@Configuration + @EnableAutoConfiguration + @ComponentScan@EnableAutoConfiguration→ Enables Boot’s auto-configuration@ComponentScan→ Scans sub-packages for Beans automatically
Web Layer (Spring MVC)
@RequestMapping→ Maps URLs to methods@GetMapping,@PostMapping,@PutMapping,@DeleteMapping→ Shorthand for HTTP methods@PathVariable→ Binds URL path variables@RequestParam→ Binds query parameters@RequestBody→ Maps request bodies (JSON/XML, etc.) to objects@ResponseBody→ Serializes return values into the HTTP Response Body@CrossOrigin→ Configures CORS
Data Access / Transactions
@Transactional→ Declarative transaction management@Entity→ JPA entity@Table→ Maps entities to database tables@Id,@GeneratedValue→ Marks primary keys@Column→ Maps entity fields to DB columns@RepositoryRestResource→ Exposes Spring Data REST repositories
Validation / Binding (Bean Validation, JSR-303/380)
@Valid,@Validated→ Enable validation@NotNull,@NotEmpty,@NotBlank→ Mandatory field validation@Size,@Min,@Max→ Size and range validation@Pattern→ Regex pattern validation@Email→ Email format validation
Others
@Bean→ Defines a Bean inside a@Configurationclass@ConditionalOnProperty,@ConditionalOnClass→ Conditional Bean registration (commonly used in Boot)@EnableScheduling/@Scheduled→ Enables scheduling tasks@EnableAsync/@Async→ Enables asynchronous execution@Profile→ Activates Beans only in specific profiles
Annotation Analysis
@Component
1 |
|
- Registers a class itself as a Bean
- Commonly applied to general Service/DAO classes
@Bean
1 |
|
- Registers the return value of a method as a Bean
- Mainly used for library objects, external dependencies, or Beans that must be manually instantiated
- Declared inside a
@Configurationclass
@Controller
1 |
|
- Marks a class as a controller
- Internally inherits from
@Component, so it is automatically registered as a Bean
@RestController
1 |
|
- Combination of
@Controllerand@ResponseBody - Method return values are directly serialized into the HTTP Response Body
@Configuration
1 |
|
- Marks a class as a Spring configuration class
- Internally includes
@Component, so it is automatically registered as a Bean
@SpringBootApplication
1 |
|
- Entry point annotation for Spring Boot applications
- A meta-annotation that combines multiple core annotations
- Enables auto-configuration, component scanning, and configuration registration when present
@SpringBootConfiguration
1 |
|
- Specialized version of
@Configuration - Marks the class as a configuration class
@EnableAutoConfiguration
1 |
|
- Core of Spring Boot
- Automatically registers Beans based on libraries present in the classpath (if predefined conditions are met)
@ComponentScan
1 |
|
- Scans the current package and subpackages for
@Component,@Service,@Controller,@Repository, etc., and registers them as Beans
Spring Data Transfer Methods
Typically, a Repository method returns the entire entity, but in many cases, not all attributes are needed in the API.
When only specific attributes are required, it is better to extract those attributes from the entity and return them using a DTO or an interface. In such cases, the appropriate approach is the Projection method.
Types of Projection include the following:
- Interface-based Projections
- Nested Projections
- Closed / Open Projections
- Use of Default Methods
- Nullable Wrappers
- DTO
Except for DTOs, the rest are interface-based Projections. For example:
1 | interface PersonSummary { |
Interface-based projections create runtime proxy objects that map an entity into a Projection.
Apart from interface-based projections, DTOs can also be used to create class-based Projections.
DTO
- Stands for Data Transfer Object, an object used to transfer data
- Not a proxy object; mapped directly through a constructor
- Contains getter/setter methods
- Commonly used between the frontend view and the backend controller

When using record, private final fields along with methods like equals and toString are automatically generated, making DTO creation very simple.
1 | record NamesOnly(String firstname, String lastname) { |
In a regular class, the constructor to be used for mapping can be specified with the @PersistenceCreator annotation.
1 | public class NamesOnly { |
- The main purpose of a DTO is to handle multiple parameters in a single call, thereby reducing server round-trips.
- Instead of directly passing an Entity to the client side (such as a Controller), data is exchanged using a DTO.
DAO
- Stands for Data Access Object, an object responsible for accessing the database
- A design pattern that separates data access logic from the rest of the application (service or business logic)
- In Spring, it is usually marked with
@Repository
1 | public interface ItemRepository extends JpaRepository<Item, Long> { |
VO
- Stands for Value Object. Unlike DTOs, which have both getters and setters, VOs only provide getters, making them readable but immutable.
- Can include behaviors (methods) that enforce domain rules
1 | public final class Email { |
Because it is declared final, it cannot be inherited, and since it has no setter, it is immutable. Equality for VOs is determined by the equality of their property values. In other words, two objects are considered the same if they share the same values.
Research for Spring CVE
CVE-2025-22223
https://spring.io/security/cve-2025-22223
This is a vulnerability caused by the incorrect use of Security Annotations in Spring Security, which can be exploited for bypass.
In an environment where @EnableMethodSecurity is enabled, if security annotations (such as @PreAuthorize, @Secured) are applied only to generic-based declarations (superclasses, interfaces) or override methods, but not to the actual target method, authentication bypass can occur.
In other words, the vulnerability arises under the following conditions:
@EnableMethodSecurityis used- Security annotations are applied only to the overridden method, while the actual target method has no annotations
As a result, the target method can be called without proper authorization.
Affected Versions
- Spring Security 6.4.0 ~ 6.4.3
- CVSS: 5.3
Part0. Environment Setup
Directory Structure
1 | ├── main |
pom.xml
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
SecurityConfig.java
1 |
|
api/AbstractSecureApi.java
1 | package com.example.demo.api; |
api/ParamApi.java
1 | package com.example.demo.api; |
Both pieces of code use the @PreAuthorize annotation to check whether the caller has the ADMIN role.
AbstractSecureApi.java: abstract class versionParamApi.java: interface version
Under normal circumstances, the @PreAuthorize annotation cannot be bypassed. However, if these classes are overridden by another class (as shown in the service code examples below), the security checks can be bypassed.
AbstractImpl.java
1 | package com.example.demo.service; |
ParamImpl.java
1 | package com.example.demo.service; |
Part1. Root Cause
When @EnableMethodSecurity is enabled, UniqueSecurityAnnotationScanner scans the hierarchy to find security annotations on the target method being called.
1 | final class UniqueSecurityAnnotationScanner<A extends Annotation> extends AbstractSecurityAnnotationScanner<A> { |
At this point, the scanner uses erasure-based signature matching such as targetClass.getDeclaredMethod(method.getName(), method.getParameterTypes()) to find the actual overridden method of the child class. With this erasure-based matching, the implemented mutate in AbstractImpl is seen as AccountSecret mutate(AccountSecret), but the overridden bridge method mutate in AbstractSecureApi is seen as Object mutate(Object). As a result, it appears as if there is no annotation, and the annotation on the parent declaration is missed.
Part2. PoC
Because of this vulnerability, in the controller below, a user with only user privileges can also access /pocA and /pocB.
1 |
|
- Log in with userID

- Accessing
/pocA,/pocB(Spring Security version 6.4.0) – vulnerability check


- Accessing
/pocA,/pocB(Spring Security version 6.4.4) – Patch Verification


Part3. Remediating and Defending
https://github.com/spring-projects/spring-security/commit/dc2e1af2dab8ef81cd4edd25b56a2babeaab8cf9
Since version 6.4.4, instead of using the previous erasure-based signature, findMethod was introduced so that security annotations on overridden methods can also be followed.
1 | - try { |
To obtain the most specific actual call target, bridge/covariant/proxy methods are resolved, and then annotations are merged and searched in the following order of priority:
- The concrete method that will actually be invoked
- If necessary, the declaring class/interface level
- If the method is a bridge, the original bridge method
- The corresponding method in a parameterized superclass/interface
With this approach, security annotations applied to generic substituted parent declarations are also treated as belonging to the target method, making bypass impossible.
CVE-2025-41232
CVE-2025-41232 is a vulnerability in certain versions of spring-security-core where the logic that detects methods with Spring security annotations was incorrectly implemented, allowing security elements to be bypassed. Notably, this vulnerability originated from code created to patch CVE-2025-22223, and since the configuration conditions required to exploit it were not complex, many Spring applications were affected. Below is the analysis of this vulnerability.
Affected
- spring security 6.4.0 - 6.4.5
- Use of
@EnableMethodSecurity(mode=ASPECTJ) - Use of spring-security-aspects
- Use of security annotations on private/protected methods, e.g.,
@PreAuthorize
- Use of
Part0. Environment Setup
Directory Structure
1 | . |
pom.xml
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
AspectjPocApplication.java
1 | package com.example.demo; |
LeakController.java
1 | package com.example.demo; |
SecretService.java
1 | package com.example.demo; |
SecurityConfig.java
1 | package com.example.demo; |
Part1. Security Annotations and Detection Method in Spring
There are two ways in Spring to protect methods. One is the Based Proxy method, which intercepts the call flow before method execution to verify authorization, and the other is the Based AspectJ method, which directly inserts security logic through bytecode weaving using AspectJ.
Based Proxy
- Uses Spring AOP (proxy-based) to intercept method calls and perform authorization checks
- Only applicable to Public Methods
- Cannot be applied to
final,private, orstaticmethods - Generally the most commonly used
Based AspectJ
- Uses AspectJ to insert authorization check logic through bytecode weaving
- When Java source is compiled,
.classfiles are generated - As the JVM loads the
.classfile into memory, the authorization logic is inserted - Allows adding new behavior (logging, authorization checks, transaction management, etc.) at runtime without modifying the original source
- Example:
- Original
1
2
3
4
5class SecretService {
String getSecret() {
return "FLAG{secret}";
}
}- After AspectJ Weaving
1
2
3
4
5
6
7
8class SecretService {
String getSecret() {
if (!SecurityContext.hasRole("ADMIN")) {
throw new AccessDeniedException();
}
return "FLAG{secret}";
}
}
- When Java source is compiled,
- Can apply annotations even to methods such as
private,final, andstatic - More powerful than the proxy method, but more complex to configure and requires the
aspectjweaverjavaagent
Weaving Types
- Compile Time Weaving (CTW)
- Inserts code when
javacgenerates.classfiles
- Inserts code when
- Post Compile Weaving (Binary Weaving)
- Weaves into already compiled
.classor.jarfiles to generate new.classfiles
- Weaves into already compiled
- Load-time Weaving (LTW)
- The
javaagentintervenes when the JVM loads classes and modifies the bytecode - Required for using AspectJ mode in Spring Security
- The
CVE-2025-41232 was discovered in the Based AspectJ method. This is because the method detection logic included in the functions called by AspectJ was incorrectly designed. To understand this, it is necessary to look at the process of scanning methods with security annotations. In general, method security annotations such as @PreAuthorize("hasRole('X')") are designed to be read and executed using reflection.
- Reflection
- A feature that allows inspection, invocation, and even modification of program structures such as classes, methods, and fields during JVM runtime
- → Enables examining and controlling program structures dynamically at runtime without hardcoding
However, reflection has high invocation costs and is particularly slow when scanning inheritance, interfaces, and bridge methods. In real services, controllers and services may be called tens of millions of times, and if annotations are read by reflection on every call, performance degradation is inevitable. Therefore, in Spring, reading and executing security annotations is implemented such that reflection scanning is only performed on the first call, and subsequent lookups are handled in O(1) time using a ConcurrentHashMap cache.
1 | // https://github.com/spring-projects/spring-security/blob/6fb0591109e3c6d9fef9ee2d1a4f215c738c22da/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java#L112 |
At this point, the function that generates cache data after the reflection scan is the merge() function. It scans method/parameter security annotations, stores them in the cache, and afterward retrieves and returns them from the cache (cache key: new MethodClassKey(method, targetClass)). In this process, the merge() function calls findMethodAnnotations(Method method, Class<?> targetClass) to scan for methods with security annotations (only scanning the annotations required during the current request handling).
1 | // https://github.com/spring-projects/spring-security/blob/6fb0591109e3c6d9fef9ee2d1a4f215c738c22da/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java#L196 |
The reason for searching methods here is that the annotated method may be declared in an interface or a superclass (since the same method could be overridden). ClassUtils.getMostSpecificMethod(...) is called to retrieve the actual method that will be executed at runtime, based on the class (targetClass) where the annotation was detected. From this method, findClosestMethodAnnotations() is then invoked.
- Since the method actually called at runtime belongs to the
targetClassimplementation, the overridden method is retrieved - Based on the retrieved method,
findClosestMethodAnnotations()is called
1 | // https://github.com/spring-projects/spring-security/blob/6fb0591109e3c6d9fef9ee2d1a4f215c738c22da/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java#L222 |
This function searches for annotations based on the specific method, because annotations may exist not only on the implementation method but also on the interface declaration or superclass. It first searches for security annotations on the specificMethod itself, and if none are found, it recursively traverses up to the superclass or interface to locate the “closest annotation.”
- This process is carried out through recursive calls
- As a result, annotations on interfaces and superclasses are also searched
- However, the function returns the first “closest annotation” it encounters
At this stage, the logic ensures that already visited classes are not revisited.
1 | if (targetClass == null || classesToSkip.contains(targetClass) || targetClass == Object.class) { |
After this, in order to find the actual Method object corresponding to the method passed as a parameter, findMethod(method, targetClass) is called based on targetClass and method.
1 | // https://github.com/spring-projects/spring-security/blob/6fb0591109e3c6d9fef9ee2d1a4f215c738c22da/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java#L268 |
Looking at the code logic, the getDeclaredMethods() function is called to retrieve all method objects declared in the targetClass, which are then iterated over and assigned to candidate. It first checks reference equality (==) between candidate and the method parameter; if they are the same reference, the candidate object is returned. After this check, the isOverride(method, candidate) function is used to compare override relationships, and depending on the result, it either returns candidate or null.
If the method object is successfully returned, the reflection invocation logic is executed based on that method, and the resulting data is added to the cache.
Part2. Root Cause
Environment Setup
To analyze the root cause, it was necessary to set breakpoints inside the spring-security-core code. Therefore, instead of using the GUI debugger in VS Code as described earlier, a Java debugging port was opened and JDB was attached for analysis.
- Application execution
1 | java -javaagent:"$WEAVER" \ |
- JDB attatch
1 | # jdb -attach 5005 |
Vulnerable Code
In fact, if Part1 was read carefully, it is clear where the vulnerability occurs. The vulnerability arises in the findMethod(method, targetClass) function.
1 | // https://github.com/spring-projects/spring-security/blob/6fb0591109e3c6d9fef9ee2d1a4f215c738c22da/core/src/main/java/org/springframework/security/core/annotation/UniqueSecurityAnnotationScanner.java#L268 |
The findMethod() function compares the method object passed as a parameter with the objects retrieved from getDeclaredMethods() using the == operator. In other words, it performs a reference equality comparison. However, even if the signatures are identical, the Method instances are different, which causes the reference equality check to fail.
Since Method is a reflection handle, different instances are created depending on how it is obtained. This means that a method object obtained through AspectJ weaving, proxies, or other code paths is not the same instance as the candidate method created by targetClass.getDeclaredMethods(). As a result, the verification fails.
This fact can be confirmed through JDB.
1 | http-nio-9999-exec-1[1] locals |
Even though the methods have the same signature, it can be confirmed that the reference equality check fails.
Part3. PoC/Exploit for CVE-2025-41232
After completing the environment setup as described in Part0, a request can be sent to the /leak endpoint to check the response value and verify that the application is vulnerable. It is also possible to observe the difference in responses depending on the spring-security-core version.
spring-security-core 6.4.5

spring-security-core 6.4.6

In spring-security-core 6.4.6 ver, 403 responsed.
Part4. Remediating and Defending
https://github.com/spring-projects/spring-security/issues/17143

The vulnerability was mitigated by changing the method searching approach.
1 | ... |
The logic was changed so that instead of performing the == comparison, it now uses equals, allowing validation even when the two objects have different references.
==
- Compares whether two variables reference the same object
- For primitive types, compares the actual value
- For object types, compares reference addresses
.equals()
- Compares whether two objects are logically equal
- Follows the
.equals()implementation overridden in each classString.equals()→ compares string contentsInteger.equals()→ compares numeric valuesMethod.equals()→ compares method signatures
CVE-2025-22233
https://spring.io/security/cve-2025-22233
CVE-2025-22233 is a vulnerability in Spring Framework where an inconsistency in case handling during the comparison and blocking process of DataBinder’s disallowedFields allows binding to occur in certain situations, bypassing disallowedFields.
Affected
- Spring Framework
- 6.2.0 – 6.2.6
- 6.1.0 – 6.1.19
- 6.0.0 – 6.0.27
- 5.3.0 – 5.3.42
- Configuration using
setDisallowedFieldsto block field binding disallowedFieldsfield name starting withi
Part0. Environment Setup
Directory Structure
1 | ├─main |
pom.xml
1 |
|
User.java
1 | package com.example.cve202522233; |
UserController.java
1 | package com.example.cve202522233; |
In Spring MVC, user input is received to create a User object and store it in DATA, while configuring data binding to exclude the role and id fields.
Cve202522233Application.java
1 | package com.example.cve202522233; |
Part1. Root Cause
1 | protected void doBind(MutablePropertyValues mpvs) { |
mpvs contains the values extracted from query strings and other request parameters.
When setDisallowedFields is configured, validation is performed inside checkAllowedFields.
1 | protected void checkAllowedFields(MutablePropertyValues mpvs) { |
Parameter values are retrieved as an array, iterated over, normalized into field names, and then checked against the allow/deny lists so that certain fields are excluded from binding.
1 | protected boolean isAllowed(String field) { |
It returns true if the allow list is empty or the field matches an allowed pattern, and the deny list is empty or the lowercased field name (ignoring locale) does not match any deny pattern.
At this point, if a field name starting with İ (\u0130) is provided, when converted to lowercase it becomes i̇ (\u0069 \u0307).
1 | public static boolean simpleMatch( String[] patterns, String str) { |
If even one of the disallowed field names matches str, the function returns true, meaning the field is excluded from binding.
1 | public static boolean simpleMatch( String pattern, String str) { |
If * is not present, the check is performed directly with pattern.equals(str).

value diff
Since the earlier lowercase conversion changes the input into i̇ (\u0069\u0307), it can bypass disallowed field names that start with i.
In the case of K, it is converted into k, so it cannot be used to bypass.
After that, the values are actually bound to the target object.
1 | protected void applyPropertyValues(MutablePropertyValues mpvs) { |
1 | public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) |
Among the values in mpvs, if the target object (for example, fields in the User class) has a matching name, the value is set; if not, it is skipped.
1 | public void setPropertyValue(PropertyValue pv) throws BeansException { |
It locates nested paths, tokenizes them, and then applies the values accordingly.
1 | //nestedPa.setPropertyValue |
If token.keys is not present, it is treated as a regular Bean field and looked up based on the field name.
1 | private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) { |
1 | protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { |
If matching fails, it retries by applying uncapitalize or capitalize to the first character.
1 |
|
Through StringUtils.uncapitalize, the first character is normalized, and İ is converted into i.
1 | public static String uncapitalize(String str) { |
1 | private static String changeFirstCharacterCase(String str, boolean capitalize) { |
In this code, when the first character is converted to lowercase and handled as a char type, the \u0307 is truncated.
Because of this, the vulnerability occurs only when the disallowed field starts with i.
Part2. PoC
In an environment where a disallowed field starting with i is registered, if the first character of the request parameter key is changed to İ (\u0130) and sent, the vulnerability can be confirmed.
6.2.6 Normal Behavior

6.2.6 Abnormal Behavior

Part3. Remediating and Defending
1 | else { |
1 | protected boolean isAllowed(String field) { |
The pre-normalization previously applied to disallowed fields and request parameters was removed, and the disallowedFields check itself was modified to be case-insensitive.
Conclusion
Through this Spring research, I was able to understand and study the code-level behavior of security annotations, model permission checks, and other Spring security elements that I had previously only understood conceptually. Although this covers only a portion, I also gained insights into the components and characteristics that make up Spring, as well as its layered structure.
In the course of the research, I realized how important it is not just to understand whether a security feature in a framework “exists or not,” but to grasp its internal mechanisms and limitations. For example, by checking at the code level how annotation-based access control scans and merges on a per-method basis, and how proxies and AOP control call flows, I gained a concrete understanding of what it really means when we say that “security logic is injected at the framework level.”
As a result, this research helped me move beyond a surface-level understanding of Spring’s security features. It highlighted the importance of asking and answering the fundamental questions: Why is this security element necessary, how does it work, and under what conditions can it be neutralized? Going forward, I recognize the strong need to continue deeper studies of framework security features that focus not only on their usage but also on their internal structures and real attack scenarios.