How to prevent IDOR attacks by using AOP in Java
3 min readApr 27, 2022
What is IDOR vulnerability in a nutshell ?
IDOR (insecure direct object references) is an access control vulnerability where unvalidated user input can be used for unauthorized access to resources or operations.
According to the explanation above we’re going to see how to prevent this type of attack with a basic example includes code snippets. Hope it will be useful !
System :
- A system using JWT stored in web browser’s cookies for authentication.
- JWT is being sent on Authorization header of HTTP request and controlled by a custom filter.
- An endpoint taking userId parameter which returns user details registered our system.(It can be any endpoint which makes some crud operations using userId parameter e.g. deleteUserByUserId etc.)
Scenario :
- After and attacker recognize this endpoint mentioned above, he can easily try to abuse this endpoint by sending http requests with userId parameters unlike his own userId gained after registration step.
- At this point if there is no control taking in charge of if this sent userId parameter, belongs to the real current user in the system, than the attacker can have unauthorized access to the most of registered user’s informations by trying to send sequential userId parameters so this can end up with terrible consequences.
Solution with code example :
To avoid this kind of trouble we need to control userId parameter using AOP (Aspect Oriented Programming) before our vulnerable endpoint take and process the request.
Let’s take a look how to do it !
- First step is creating an encrypted string called “ticket” and than putting the “ticket” as a “claim” to JWT. Later by using this ticket we will understand if the user is really an authorized user . We are doing that by salting with a secret string our ticket before encryption.
@Component
public class EncryptionManager {
private final String salt = "MY-SECRET";
public String encryptTicket(String userId) {
if(!StringUtils.hasText(userId)) {
return "";
}
return DigestUtils.sha256Hex(userId + salt);
}
}claims.put("ticket",encryptionManager.encryptTicket(String.valueOf(userId;
- Define an annotation type interface with parameterIndex parameter. (We will see the usage in next steps)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IdGuard {
int parameterIndex();
}
- Put the annotation above the endpoint method which needs to be conrolled before accessing the resources.
@IdGuard(parameterIndex = 0) // parameterIndex = 0 -> this means, get the first indexed parameter as an argument for this method. Here is "id".
@GetMapping("/getUserById")
public UserDto findUserById(@PathParam("id") Long id){
UserDto userById = userService.findUserById(id);
return userById;
}
- Now add Aspectj as a depencency to pom.xml file (I’ve used spring-boot-starter-aop artifact which includes Aspectj lib. in itself)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- Create IdAspect class and tag with @Aspect annotation.
@Aspect
@Component
public class IdAspect {
- Write a method in this class and tag with @Before annotation. Pass idGuard annotation that we defined before as an argument. So this method will be executed before the execution of any method tagged with @idGuard annotation .
- Call the encryptTicket method by providing encryptionManager and get the expectedTicket and than compare with the realTicket placed in JWT before. These two tickets must be an exact match else it means the attacker sending a different userId parameter to our endpoint.
- In this case throw a RunTimeException !
@Before("@annotation(idGuard)")
public void execute(JoinPoint joinPoint, IdGuard idGuard) {
int argIndex = idGuard.parameterIndex();
if(argIndex < 0){
argIndex = 0;
}
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
throw new RuntimeException("ACCESS DENIED");
}
Object idParameterObj = args[argIndex];
if(idParameterObj == null) {
throw new RuntimeException("ACCESS DENIED");
}
String expectedTicket = encryptionManager.encryptTicket(idParameterObj.toString());
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String token = tokenManager.extractJwtFromRequest(request);
if(!StringUtils.hasText(token)) {
throw new RuntimeException("ACCESS DENIED - INVALID RESOURCE");
}
String realTicket = tokenManager.extractTicket(token);
if(!(StringUtils.hasText(expectedTicket) && expectedTicket.contains(realTicket)) ) {
throw new RuntimeException("ACCESS DENIED - INVALID RESOURCE");
}
}