Securing Spring Boot Web Applications

This article applies to sites created with the Spring Boot framework. We will be discussing the following four methods to add additional layers of security to Spring Boot apps:

  • Preventing SQL Injection using Parameterized Queries
  • URL Parameter Input Validation
  • Form Field Input Validation
  • Output Encoding to Prevent Reflected XSS Attacks

I use these methods for my website, Initial Commit, which is built using Spring Boot, the Thymeleaf template engine, Apache Maven, and is hosted on AWS Elastic Beanstalk.

In our discussion of each security tip, we'll first describe an attack vector to illustrate how a relevant vulnerability might be exploited. We'll then outline how to secure the vulnerability and mitigate the attack vector. Note that there are many ways to accomplish a given task in Spring Boot – these examples are suggested to help you better understand potential vulnerabilities and methods of defense.

Preventing SQL Injection using Parameterized Queries

SQL Injection is a common and easy to understand attack. Attackers will try to find openings in your app's functionality that will allow them to modify the SQL queries that your app submits to the database, or even submit their own custom SQL queries. The attacker's goal is to access sensitive data that is stored in the database, which shouldn't be accessible through normal app usage, or to cause irreparable damage to the system under attack.

One common way that an attacker will try to inject SQL into your app is through URL parameters that are used to build SQL queries that get submitted to the database. For example consider the following example URL:

https://fakesite.com/getTransaction?transactionId=12345  

Let's say that there is a Spring Boot controller endpoint defined at /getTransaction which accepts a transaction ID in the URL parameter:

@GetMapping("/getTransaction")
public ModelAndView getTransaction(@RequestParam("transactionId") String transactionId) {

    ModelAndView modelAndView = new ModelAndView();

    sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = " + transactionId;

    Transaction transaction = jdbcTemplate.query(sql, new TransactionRowMapper());

    modelAndView.addObject("transaction", transaction);
    modelAndView.setViewName("transaction");

    return modelAndView;
}

Notice that the SQL statement in this example is built using string concatenation. The transactionId is simply tacked on after the "WHERE" clause using the + operator.

Now imagine an attacker uses the following URL to access the site:

https://fakesite.com/getTransaction?transactionId=12345;+drop+table+transaction;  

In this case, the URL parameter transactionId (which is defined as a String in our controller method) is manipulated by the attacker to add in a "DROP TABLE" statement, so the following SQL will be run against the database:

SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = 12345; drop table transaction;  

This would drop the transaction table leading to a broken app and possibly irreparable data loss, due to the fact that the SQL statement accepts the user-supplied URL parameter and runs it as live SQL code.

In order to remedy the situation, we can use a feature called parameterized queries. Instead of concatenating our dynamic variables directly into SQL statements, parameterized queries recognize that an unsafe dynamic value is being passed in, and uses built-in logic to ensure all user-supplied content is escaped. This means that variables passed in through parameterized queries will never execute as live SQL code.

Here is a version of the affected code snippets above, updated to use parameterized queries:

sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = ?";

Transaction transaction = jdbcTemplate.query(sql, new TransactionRowMapper(), transactionId);  

Notice the replacement of the + operator and transactionId variable directly in the SQL statement. These are replaced by the ?, which represents a variable to be passed in later. The transactionId variable is passed in as an argument to the jdbcTemplate.query() method, which knows that all parameters passed in as arguments need to be escaped. This will prevent any user input from being processed by the database as live SQL code.

Another format for passing parameterized queries in Java is the NamedParameterJdbcTemplate. This presents a clearer way to identify and keep track of the variables passed through the queries. Instead of using the ? symbol to identify parameters, the NamedParameterJdbcTemplate uses a colon : followed by the name of the parameter. Parameter names and values are kept track of in a map or dictionary structure, as seen below:

Map<String, Object> params = new HashMap<>();

sql = "SELECT transaction_user, transaction_amount FROM transaction WHERE transaction_id = :transactionId";

params.put("transactionId", transactionId);

Transaction transaction = jdbcTemplate.query(sql, params, new TransactionRowMapper());  

This example behaves identically to the previous one, but it is more popular due to the clarity it affords in identifying the parameters in a SQL statement. This is especially true in more complex SQL statements that would have large numbers of ? which need to be checked to make sure they are in the right order.

URL Parameter Input Validation

When thinking about app security, a primary consideration is listing all points at which the app accepts input from users. Each input point can be vulnerable if not properly secured and as developers we need to expect that attackers will attempt to exploit all input sources.

One common way that apps receive input data from users is directly from the URL string in the form of URL parameters. The sample URL we used in the previous section is an example of passing in a transactionId as a URL parameter:

https://fakesite.com/getTransaction?transactionId=12345  

Let's assume we want to ensure that the transaction ID is a number and that it falls within the range of 1 and 100,000. This is a simple two-step process:

Add the @Validated annotation on the controller class that the method lives in.

Use inline validation annotations directly on the @RequestParam in the method argument, as follows:

@GetMapping("/getTransaction")
public ModelAndView getTransaction(@RequestParam("transactionId") @min(1) @max(100000) Integer transactionId) {  
    // Method content
}

Note that we changed the type of the transactionId to Integer from String, and added the @min and @max annotations inline with the transactionId argument to enforce the specified numeric range.

If the user supplies an invalid parameter that doesn't meet these criteria, a javax.validation.ContractViolationException is thrown which can be handled to present the user with an error describing what they did wrong.

Here are a few other commonly used constraint annotations used for URL parameter validation:

  • @Size: the element size must be between the specified boundaries.
  • @NotBlank: the element must not be NULL or empty.
  • @NotNull: the element must not be NULL.
  • @AssertTrue: the element must be true.
  • @AssertFalse: the element must be false.
  • @Past: the element must be a date in the past.
  • @Future: the element must be a date in the future.
  • @Pattern: the element must match a specified regular expression.

Form Field Input Validation

Another more obvious type of user input comes from form fields presented to end users for the specific purpose of gathering information to be saved in the database or processed by the application in some way. Some examples of form fields are text boxes, check boxes, radio buttons, and dropdown menus.

Usually form field input is transmitted from client to server via a POST request. Since form data usually includes arbitrary user input, all input field data must be validated to make sure it doesn't contain malicious values that could harm the application or expose sensitive information.

Let's assume that we're working with a veterinary web application that has a web form allowing end users to sign up their pet. Our Java code would include a domain class that represents a pet, as follows:

@Entity
public class Pet {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    @NotBlank(message="Name must not be empty")
    @Size(min=2, max=40)
    @Pattern(regexp="^$|[a-zA-Z ]+$", message="Name must not include special characters.")
    private String name;

    @NotBlank(message="Kind must not be empty")
    @Size(min=2, max=30)
    @Pattern(regexp="^$|[a-zA-Z ]+$", message="Kind must not include special characters.")
    private String kind;

    @NotBlank(message="Age must not be empty")
    @Min(0)
    @Max(40)
    private Integer age;

    // standard getter and setter methods...
}

Note the constraint annotations that have been included over each field. These work the same way as described in the previous section, except we have specified a message for some of them, which will override the default error messages displayed to the user when the respective constraint is violated.

Note that each field has annotations that specify the range that the field should fall in. In addition, the String fields (name and kind) have a @Pattern annotation, which implements a regex constraint that only accepts letters and spaces. This prevents attackers from trying to include special characters and symbols, which may have significance in code contexts like the database or browser.

The HTML form contains the corresponding Pet class' fields, including the pet's name, kind of animal, age, and might look something like below:

Note that this HTML snipped includes Thymeleaf template tags to markup the HTML.

<form id="petForm" th:action="@{/submitNewPet}" th:object="${pet}" method="POST">  
    <input type="text" th:field="*{name}" placeholder="Enter pet name…" />

    <select th:field="*{kind}">
        <option value="cat">Cat</option>
        <option value="dog">Dog</option>
        <option value="hedgehog">Hedgehog</option>
    </select>

    <input type="number" th:field="*{age}" />

    <input type="submit" value="Submit Form" />
</form>  

When the form fields are filled out and the "Submit" button is clicked, the browser will submit a POST request back to the server at the "/submitNewPet" endpoint. This will be received by an @RequestMapping method, defined as follows:

@PostMapping("/submitNewPet")
public ModelAndView submitNewPet(@Valid @ModelAttribute("pet") Pet pet, BindingResult bindingResult) {

    ModelAndView modelAndView = new ModelAndView();

    if (bindingResult.hasErrors()) {
        modelAndView.addObject("pet", pet);
        modelAndView.setViewName("submitPet");
    } else {
        modelAndView.setViewName("submitPetConfirmation");
    }

    return modelAndView;
}

The @Valid annotation on the method argument will enforce the validations defined on the Pet domain object. The bindingResult argument is handled automatically by Spring and will contain errors if any of the model attributes have constraint validations. In this case, we incorporate some simple login to reload the submitPet page if constraints are violated and display a confirmation page if the form fields are valid.

Output Encoding to Prevent Reflected XSS Attacks

The final security topic we are going to discuss is Output Encoding of user-supplied input and data retrieved from the database.

Imagine a scenario where an attacker is able to pass in a value as input through a URL parameter, form field, or API call. In some cases this user-supplied input could be passed as a variable straight back to the view template that is returned to the user, or it could be saved in the database.

For example, the attacker passes in a string that is valid Javascript code such as:

alert('This app has totally been hacked, bro');  

Let's consider the scenarios where the above string gets saved into a database field as a comment, later to be retrieved in the view template and displayed to the user in their Internet browser. If the variable is not properly escaped, the alert() statement will actually run as live code as soon as the page is received by the user's browser – they will see the alert pop up. While annoying, in a real attack this code would not be an alert, it would be a malicious script that could trick the user into doing something nasty.

In fact, the malicious user-supplied content doesn't necessarily need to be saved to the database to cause harm. In many cases, user-supplied input, such as usernames, is essentially echoed back to the user to display on the page they are visiting. These are called "reflected" attacks for this reason, since the malicious input is reflected back to the browser where it can do harm.

In both of these cases, dynamic content needs to be properly Output Encoded (or escaped) in order to make sure it isn't processed by the browser as live Javascript, HTML, or XML code.

This can be accomplished easily by using a mature template engine, such as Thymeleaf. Thymeleaf can be easily integrated into a Spring Boot app by adding the required POM file dependencies and performing some minor configuration steps that we won't go into here. The th:text attribute in Thymeleaf has built-in logic that will handle the encoding of any variables that are passed into it as follows:

<h1>Welcome to the Site! Your username is: <span th:text="${username}"></span></h1>  

In this case, even if the username variable contained malicious code such as alert('You have been hacked');, the text would just be displayed on the page instead of being executed as live Javascript code by the browser. This is due to Thymeleaf's built-in encoding logic.

About the Author

This article was written by Jacob Stopak, a software consultant and developer with passion for helping others improve their lives through code. Jacob is the creator of Initial Commit - a site dedicated to helping curious developers learn how their favorite programs are coded. Its featured project helps people learn Git at the code level.

Author image
About Jacob Stopak
San Diego, CA Twitter Website
Jacob Stopak is a software developer and creator of InitialCommit.io - a site dedicated to teaching people how popular programs are coded. Its main project helps people learn Git at the code level.