DEV Community

Cover image for Spring Boot validation and bean manipulation
Elattar Saad
Elattar Saad

Posted on

Spring Boot validation and bean manipulation

Secure applications follow several security measures during their dev and prod phases, the app entry points are one of the most important parts to secure due to the risk of data injection that may occur. Spring proposes its way of data validation which improves controlling these particular points.

As you've already guessed, this article is about more than just data validation; it's also about bean or custom data structure manipulation, which are two important applicative security features that every developer should be aware of.

Enough introductions, for now, let's start with a global blueprint of what we need to achieve, you need to know that both mechanisms resolve two security vulnerabilities:

  • Injection attacks
  • Database schema exposure

Sounds bad right? Well, the solution is actually simpler than you think. First, let's understand both problems before jumping to the solutions.

Injection attacks

An injection attack is when malicious code is injected into the network and retrieves all of the data from the database and sends it to the attacker.
As you concluded the attack uses the open door of your app to gain all the stored data, and it can disguise, the attack, in many types such as XSS, SQL, XPath, Template, code, CRLF, LDAP, OS command injections, and more.
If you're using ORM operations you're not totally safe, but you're one step ahead. The more defenses you raise the better.

Database schema exposure

Mainly when this occurs, it doesn't come with a great benefit for the attackers, but it still delivers a piece of valuable information, which is the way you constructed your schemas, data types, and relations, in some scenarios it becomes critical.

Data validation

During this tutorial, we will be based on our last Movies API.

First, let's add the Spring boot validation dependency to our pom.xml file.

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-validation</artifactId> 
</dependency>
Enter fullscreen mode Exit fullscreen mode

Second, we'll enhance our movie model with some validation rules.

// other imports
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
@Entity
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @NotNull(message = "Id must not be null")
    private Long id;

    @NotBlank(message = "Name must not be blank")
    private String name;

    @NotBlank(message = "Type must not be blank")
    private String type;

    @Min(value = 1, message = "Movies are mainly more than a minute")
    @Max(value = 300, message = "Movies are less than 5 hours")
    private Long duration;

    @NotNull(message = "Release year must not be null")
    private Long releaseYear;

}

Enter fullscreen mode Exit fullscreen mode

Third, we alter our controller end points' signatures so they can validate incoming request bodies and throw Exception instead of custom exceptions.

...

import javax.validation.Valid;

@RestController
@RequestMapping("movie")
@Data
public class MovieController {

    ...

    @PostMapping("/")
    public ResponseEntity<?> save(@Valid @RequestBody Movie movie) throws Exception {
        if (movie == null)
            return ResponseEntity.badRequest().body("The provided movie is not valid");
        return ResponseEntity.status(HttpStatus.CREATED).body(movieService.save(movie));
    }

    @PutMapping("/")
    public ResponseEntity<?> update(@Valid @RequestBody Movie movie) throws Exception {
        if (movie == null)
            return ResponseEntity.badRequest().body("The provided movie is not valid");
        return ResponseEntity.ok().body(movieService.update(movie));
    }

    ...

}

Enter fullscreen mode Exit fullscreen mode

The @Valid makes sure that the incoming body is valid, otherwise a MethodArgumentNotValidException will be thrown

Obviously, we removed the custom-made exception and replaced it with the Exception class so the controller won't suppress the exception in the controller layer and let the default exception handler interfere, which will cause a 500 server internal error. Instead, it will be handled by the exception handler we’re about to make.

package io.xrio.movies.controller.advice;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException exception) {
        Map<String, String> errors = new HashMap<>();
        exception.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.badRequest().body(errors);
    }
}

Enter fullscreen mode Exit fullscreen mode

Just like our previous Movie exception handler, this ControllerAdvice based handler will target the MethodArgumentNotValidException type exceptions and resolve them by retrieving the validation violation, wrap it in a response entity and send it back to the user with 400 bad request response code.

NOTE: Returning the validation violations to the user is a huge blender, it's like telling a house robber why they failed to rob your own house.

To counter this, we'll print them on the logs, which are by the way are accessible only to the prod env admins. Our Exception will be like this:

import lombok.extern.slf4j.Slf4j;

...

@ControllerAdvice
@Slf4j
public class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException exception) {
        exception.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            log.error(fieldName + ": " + errorMessage);
        });
        return ResponseEntity.badRequest().body("Sorry, that movie you sent sucks :)");
    }
}

Enter fullscreen mode Exit fullscreen mode

Sending the same request will result this:

And only prod env admins can see this:

The @Slf4j is a short way using Lombok to call the logger.

Data validation not only is a layer to counter injection attacks, but it helps keep your data nice and clean.

Since we still exposing our model in our end-points, it's time to change that!

Data manipulation

Introduction to the DTOs

The data transfer objects, also known as Value Objects (VOs), will be the ones carrying data between two processes, in our case, it will be their structure that will be exposed instead of the model's.

Our MovieDTO will be like the following:

import lombok.Data;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class MovieDTO {

    @NotNull(message = "Id must not be null")
    private Long id;

    @NotBlank(message = "Name must not be blank")
    private String name;
    @NotBlank(message = "Type must not be blank")
    private String type;
    @Min(value = 1, message = "Movies are mainly more than a minute")
    @Max(value = 300, message = "Movies are less than 5 hours")
    private Long duration;
    @NotNull(message = "Release year must not be null")
    private Long releaseYear;

}

Enter fullscreen mode Exit fullscreen mode

Since the DTOs are the ones to be exposed, we added some validation rules.

Okay, we have the DTOs exposed, but how are we going to persist data using DTOs?

The answer is that DTOs only exist in the controller layer, in other words, we can't use them on the service and repository layers.

Also means our need for a conversion mechanism, yes, we need a MovieConverter.

Let's start with the integration of the ModelMapper dependency which will help convert models and DTO in both ways:

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>2.3.5</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Then, we add its basic configuration so it will treated as Spring Bean:


import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ModelMapperConfig {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}

Enter fullscreen mode Exit fullscreen mode

I don't like repeating myself (DRY priciple) that's why I always put redundant behavior into generic classes, the AbstractConverter will do that for us:


import java.util.ArrayList;
import java.util.List;

public abstract class AbstractConverter <DM, DTO> {

    public abstract DM convertToDM(DTO dto);

    public abstract DTO convertToDTO(DM dm);

    public List<DM> convertToDMs(List<DTO> dtos) {
        List<DM> dms = new ArrayList<>();
        for (DTO dto : dtos) dms.add(convertToDM(dto));
        return dms;
    }

    public List<DTO> convertToDTOs(List<DM> dms) {
        List<DTO> dtos = new ArrayList<>();
        for (DM dm : dms) dtos.add(convertToDTO(dm));
        return dtos;
    }

}

Enter fullscreen mode Exit fullscreen mode

Our MovieConverter will inherit from the AbstractConverter with the movie model and DTO as class params.

import io.xrio.movies.dto.MovieDTO;
import io.xrio.movies.model.Movie;
import org.modelmapper.ModelMapper;
import org.modelmapper.config.Configuration;
import org.springframework.stereotype.Component;

@Component
public class MovieConverter extends AbstractConverter<Movie, MovieDTO> {

    private final ModelMapper modelMapper;

    public MovieConverter(ModelMapper modelMapper) {
        modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE);
        this.modelMapper = modelMapper;
    }

    @Override
    public Movie convertToDM(MovieDTO movieDTO) {
        return modelMapper.map(movieDTO, Movie.class);
    }

    @Override
    public MovieDTO convertToDTO(Movie movie) {
        return modelMapper.map(movie, MovieDTO.class);
    }
}
Enter fullscreen mode Exit fullscreen mode

I decorated the MovieConverter with the @Component annotation so it can be injected into the MovieController later.

The model mapper will be configured in the MovieConverter constructors with a simple configuration, and with the model and DTO having the same fields, the mapping will be possible for now.

Before testing our converter, we need to inject it into the controller, then alter the endpoints so they can handle the DTOs now instead.

package io.xrio.movies.controller;

import io.xrio.movies.converter.MovieConverter;
import io.xrio.movies.dto.MovieDTO;
import io.xrio.movies.service.MovieService;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("movie")
@Data
public class MovieController {

    final MovieService movieService;
    final MovieConverter movieConverter;

    @PostMapping("/")
    public ResponseEntity<?> save(@Valid @RequestBody MovieDTO movieDTO) throws Exception {
        if (movieDTO == null)
            return ResponseEntity.badRequest().body("The provided movie is not valid");
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(movieConverter.convertToDTO(movieService.save(movieConverter.convertToDM(movieDTO))));
    }

    @PutMapping("/")
    public ResponseEntity<?> update(@Valid @RequestBody MovieDTO movieDTO) throws Exception {
        if (movieDTO == null)
            return ResponseEntity.badRequest().body("The provided movie is not valid");
        return ResponseEntity
                .ok()
                .body(movieConverter.convertToDTO(movieService.update(movieConverter.convertToDM(movieDTO))));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable Long id) throws Exception {
        if (id == null)
            return ResponseEntity.badRequest().body("The provided movie's id is not valid");
        return ResponseEntity.ok().body("Movie [" + movieService.delete(id) + "] deleted successfully.");
    }

    @GetMapping("/")
    public ResponseEntity<List<MovieDTO>> findAll() {
        return ResponseEntity.ok().body(movieConverter.convertToDTOs(movieService.findAll()));
    }

}

Enter fullscreen mode Exit fullscreen mode

Then we test it!

Great! But it looks the same as before with more code!

True, so let's play the same game with different rules, the movie model for us now will be different than the DTO in order to protect our API against the database schema exposure:

Some of the movie model fields will be put in an info class that will be embedded in the movie, that way we will change the structure and keep things simple for you.

NOTE: I removed the model validation because it's no longer used or needed.


import lombok.Data;

import javax.persistence.*;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;

@Data
@Entity
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Embedded
    private Info info;

}

Enter fullscreen mode Exit fullscreen mode
import javax.persistence.Embeddable;

@Embeddable
public class Info {

    private String name;
    private String type;
    private Long duration;
    private Long releaseYear;

}

Enter fullscreen mode Exit fullscreen mode

We need also to alter our DTO's fields' names so we don't need to add an advanced configuration for the model mapper.


import lombok.Data;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class MovieDTO {

    @NotNull(message = "Id must not be null")
    private Long id;

    @NotBlank(message = "Name must not be blank")
    private String infoName;
    @NotBlank(message = "Type must not be blank")
    private String infoType;
    @Min(value = 1, message = "Movies are mainly more than a minute")
    @Max(value = 300, message = "Movies are less than 5 hours")
    private Long infoDuration;
    @NotNull(message = "Release year must not be null")
    private Long infoReleaseYear;

}

Enter fullscreen mode Exit fullscreen mode

And finally testing it.

Finnally

Data validation and data manipulation can be a greatly valued asset to your API dev and prod, not only enhancing security but also gives you the power to keep your data in shape and to adapt to the users’ requirements without changing your own.

Find the source code Here.

More articles Here.

Discussion (0)