When it comes to web applications, security is a paramount concern, and the Open Web Application Security Project (OWASP) offers essential guidelines to fortify your app against various vulnerabilities.
In this article, we'll focus on basic user input validation using Java and Vaadin. For more information, plain Java examples, and tutorials on secure coding practices, visit github.com/secure-coding-practices.
Vaadin application security landscape
Vaadin-based applications are generally very secure. If you have adhered to the architecture and used most of the server-side Java API components, your code and validations are secure on the server. The framework handles basic elements such as character sets, URLs, and parameter encodings. Additionally, Java provides you with compile-time type checks for data types.
However, in modern apps, much of the input is in the form of untyped strings. While your application code is secure, the user input may not be safe, making validating all data in the application essential. Proper string input validation can protect your application from various forms of attacks, such as SQL injection, cross-site scripting (XSS), and command injection. Let’s look at the extra steps to improve the security of your application for this kind of user input.
(Please note that while this article focuses on user input validation, these rules also apply to input from other systems. You should always validate the integrity of data coming from external systems.)
Basic input validation
Let's start with a practical example of a TextField where a user can input their username. We need to validate the input using Vaadin's built-in validation features to prevent potential vulnerabilities. In this case, we will set a validator to allow only alphanumeric characters in the username field:
String USERNAME_PATTERN = "^[a-zA-Z0-9_]{3,20}$";
TextField usernameField = new TextField("Username");
usernameField.setPattern(USERNAME_PATTERN);
This pattern ensures that the username consists only of alphanumeric characters and underscores and is between 3 and 20 characters long. In this example, we are using a local variable, but it is recommended to define the patterns as a single final constant and reuse them for consistency.
Note that poorly crafted regular expression is used in pattern matching can lead to extreme performance issues with nested quantifiers or excessive backtracking. These can escalate to situation where CPU is busy processing crafted inputs. This could lead to Regular Expression Denial of Service (ReDoS). Mitigating ReDoS vulnerabilities involves careful construction and analysis of regular expressions.
Programmatic validation
While pattern validation makes a good start, typical validations often involve more complexity, such as custom validation logic, checking if a value already exists in the database, and calling external services for validation.
Consider a scenario when the user clicks the ‘Validate ISBN’ button; it provides immediate feedback to the user. If the ISBN is valid, it shows a success notification; otherwise, it indicates an error.
Button validateButton = new Button("Validate ISBN", event -> {
String isbn = isbnField.getValue();
isbnField.setInvalid(false);
if (InputValidation.validateISBN(isbn) == ValidationResult.OK) {
Notification.show("Valid ISBN format");
} else {
Notification.show("Invalid ISBN format");
isbnField.setInvalid(true);
}
});
`
The validation logic is encapsulated in the InputValidation.validateISBN(isbn)
method, which returns a ValidationResult
enum. This approach displays a generic invalid input message using Notification and sets the status of the field. In cases of more complex validation, the ValidationResult
can be extended to provide additional information about the underlying error.
This same approach can be applied when using the ValueChangeListener
in TextField
:
isbnField.addValueChangeListener(event -> {
String isbn = event.getValue();
event.getSource().setInvalid(false);
if (InputValidation.validateISBN(isbn) == ValidationResult.OK) {
Notification.show("Valid ISBN format");
} else {
Notification.show("Invalid ISBN format");
event.getSource().setInvalid(true);
}
});
This provides faster feedback to the user as the value is typed in. To improve this validation, consider moving the logic into a custom ValueChangeListener
class or even implementing a custom Validator
to be used together with the Binder
.
Declarative Bean Validation
Bean Validation API is a Java specification for validating beans through annotations. It simplifies the process of ensuring that the properties of your JavaBeans meet certain criteria, using annotations such as @NotNull
and @Size
. This standard is particularly useful in enterprise applications for maintaining data integrity and enforcing business rules.
To use JSR-303 annotations, you need to include a Bean Validation implementation in your project, such as Hibernate Validator:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
The above username validation can be expressed with the following annotation in a User class:
public class User {
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "Username must be alphanumeric")
private String username;
/* … */
}
And this can be used in a TextField
using BeanValidationBinder
:
TextField beanUsernameField = new TextField("Username");
Binder<User> userBinder = new BeanValidationBinder<>(User.class);
userBinder.forField(beanUsernameField).bind("username");
User user = new User();
userBinder.setBean(user);
getContent().add(beanUsernameField);
JSR-303 bean validation is a powerful tool for systematically enforcing business rules and data integrity in a Java application, helping you to keep your data consistent and valid with minimal boilerplate code.
Conclusion
In software development, validating user input is not just a feature; it's necessary to maintain the trust and safety of your users and your application. Adding secure validation is easy when you remember a few basic rules:
- Make all the validations in Java on the server side. While client-side validations enhance the user experience, they should not be relied upon.
- Use known validations.
- All validation failures should result in input rejection. Do not store invalid data; display it in error messages or logs.
- Whitelisting characters is more secure than blacklisting, as it only allows known safe characters. This is particularly important for usernames and passwords.
- Use Vaadin Binder and JSR-303 for comprehensive server-side validation. These are standard tools for enforcing common rules consistently across your data.
For more information and examples in Java, please visit github.com/secure-coding-practices.
Don't forget to check out part two of this series, where we'll focus on binary data input validation using Java and Vaadin.