Secure Your Java Apps: Input Validation with Quarkus Made Easy
Master OWASP-compliant validation using Hibernate Validator and Qute in a modern Quarkus stack. Complete with real code, form handling, internationalization, and pattern matching.
Input validation is one of the most effective security controls available to developers. The OWASP Input Validation Cheat Sheet recommends a combination of techniques ranging from allowlists and canonicalization to server-side validation. Goal is to reduce the attack surface of applications.
In this article, we’ll walk through how to apply these recommendations in Quarkus using Jakarta Bean Validation (Hibernate Validator). We’ll build a real-world example that includes input validation with @Pattern
and a simple, internationalized Qute-based frontend form. You can find the working example and source code in my Github repository.
Prerequisites
Before you start, make sure you have:
Java 17 or higher installed
Maven 3.9+ installed
Quarkus CLI installed
If you don't have the Quarkus CLI, you can also use Maven directly, but the CLI makes setup faster.
Creating the Quarkus Application
Start by creating a new Quarkus application with the necessary extensions:
quarkus create app com.example:validation-example \
--extension=rest-jackson,quarkus-qute-web
This will scaffold a project with:
REST with Jackson (for REST API and form data handling)
Qute (for server-side HTML templating)
Navigate into your new project:
cd validation-example
Add the Hibernate Validator extension:
quarkus add extension hibernate-validator
You're ready to start coding.
Why Input Validation Matters
Poor input validation is a root cause of many vulnerabilities, including:
SQL Injection
Cross-site Scripting (XSS)
Command Injection
Buffer Overflows
Unexpected logic failures
The golden rule: never trust user input. Always validate it on the server.
Building a User Registration Example
We’ll build a simple user registration form that validates:
Username: 4–20 characters, non-empty
Email: valid email format
Phone number: matches US format
(123) 456-7890
The UserRegistration DTO
The UserRegistration class acts as a Data Transfer Object (DTO) for the registration form. It defines the fields users are expected to submit, like username, email, and phone number, and applies validation constraints using Jakarta Bean Validation annotations. Each constraint enforces rules on the input, ensuring that invalid data is caught and handled before reaching any business logic. This approach keeps the validation logic cleanly separated from the resource layer and makes it easy to maintain and extend..
package com.example;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.ws.rs.FormParam;
public class UserRegistration {
@FormParam("username")
@NotBlank(message = "Username is required")
@Size(min = 4, max = 20, message = "Username must be 4 to 20 characters")
private String username;
@FormParam("email")
@NotBlank(message = "Email is required")
@Email(message = "Must be a valid email")
private String email;
@FormParam("phone")
@Pattern(
regexp = "^\\(\\d{3}\\) \\d{3}-\\d{4}$",
message = "Phone must match (123) 456-7890 format"
)
private String phone; // Getters and setters
}
Bean Validation uses Jakarta Validation (formerly Java Bean Validation)
Annotations define rules on fields
@NotBlank: Ensures a field is not null and not empty
@Size: Validates string length (min and max)
@Email: Validates email format
@Pattern: Validates against a regular expression
Rules are checked automatically
Form Binding:
@FormParam binds form fields to Java object properties
@BeanParam automatically creates and validates the object
Error Messages:
Each validation annotation has a message attribute
Messages are shown to users when validation fails
The RegistrationResource
The RegistrationResource class defines the backend logic for displaying the registration form and handling user submissions. It exposes two endpoints: one for rendering the form (GET /register) and one for processing form data (POST /register). When a user submits the form, the resource validates the input using Jakarta Bean Validation. If validation errors are found, they are passed back to the Qute template to be displayed next to the corresponding form fields. This ensures that all validation is performed securely on the server side:
package com.example;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/")
public class RegistrationResource {
@Inject
Template registration;
@Inject
Validator validator;
@Path("/registration")
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance showForm() {
return registration.data("registration", new UserRegistration())
.data("validation", Map.of(
"username", "",
"email", "",
"phone", ""));
}
@Path("/register")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public TemplateInstance handleForm(@BeanParam UserRegistration registrationDto) {
Set<ConstraintViolation<UserRegistration>> violations = validator.validate(registrationDto);
Map<String, String> validationMessages = new HashMap<>();
// Initialize with empty messages for all fields
validationMessages.put("username", "");
validationMessages.put("email", "");
validationMessages.put("phone", "");
if (!violations.isEmpty()) {
// Override with actual validation messages where there are violations
violations.forEach(violation -> validationMessages.put(
violation.getPropertyPath().toString(),
violation.getMessage()));
return registration
.data("registration", registrationDto)
.data("validation", validationMessages);
}
// Normally you would persist user here
return registration
.data("registration", registrationDto)
.data("validation", validationMessages);
}
}
The Qute Template: src/main/resources/templates/registration.html
The user registration form is rendered using a Qute template. Qute is Quarkus’s native templating engine, designed for fast server-side HTML generation with type-safe templates. In our example, the registration.html file displays the registration form fields and dynamically injects validation error messages for each field if any validation fails. This way, users receive immediate feedback after form submission, all handled server-side without needing JavaScript. Let’s look at the template file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>User Registration</title>
</head>
<body>
<h1>User Registration</h1>
<form method="post" action="/register">
<label>Username:</label><br>
<input type="text" name="username" value="{registration.username ?: ''}"><br>
<span style="color:red">{validation['username']}</span><br>
<label>Email:</label><br>
<input type="text" name="email" value="{registration.email ?: ''}"><br>
<span style="color:red">{validation['email']}</span><br>
<label>Phone:</label><br>
<input type="text" name="phone" value="{registration.phone ?: ''}"><br>
<span style="color:red">{validation['phone']}</span><br>
<button type="submit">Register</button>
</form>
</body>
</html>
This template:
Binds field values back into the form if validation fails
Displays field-specific validation errors inline
Why @Pattern
is Powerful
The @Pattern
annotation lets you enforce exact formats. This defends against:
Input tampering
Injection attacks hidden inside unexpected characters
Mistakes like wrong phone or ID formats
Example: Validating a product code:
@Pattern(regexp = "^[A-Z0-9]{8}$", message = "Product code must be 8 uppercase letters or digits")
private String productCode;
Additional Validation Scenarios
Beyond basic field validation, real-world applications often require handling more complex input scenarios. Here are common examples and how to approach them:
International phone numbers:
Instead of hardcoding patterns, use a library like Google's libphonenumber to validate and format international numbers reliably across different country codes.
Credit card numbers:
Apply a Luhn algorithm check in a custom validator to ensure the number is not only correctly formatted but also mathematically valid.
Dates and times:
Validate input against ISO-8601 formats (e.g., yyyy-MM-dd for dates) to avoid parsing issues and ensure compatibility with APIs and databases.
Uploaded file names:
Normalize file names to a canonical form and restrict allowed file extensions to prevent path traversal attacks or executable file uploads.
Cross-field dependencies:
Use class-level custom constraints when two or more fields must satisfy a condition together, such as ensuring password and confirm password fields match.
Canonicalization Example
Before validating, normalize input to a standard format:
String canonicalPhone = phone.trim().replaceAll("[^\\d]", "");
// Validate canonicalPhone instead of raw input
This prevents attackers from bypassing validations with encoded or obfuscated inputs.
Add Localization To Error Messages
In Quarkus, we can use the ValidationMessages.properties file to customize validation messages. Let me check if we have the necessary files and then make the required changes.
First, let's create a messages properties file (resources/ValidationMessages.properties):
jakarta.validation.constraints.NotBlank.message=This field is required
jakarta.validation.constraints.Size.message=Size must be between {min} and {max} characters
jakarta.validation.constraints.Email.message=Please enter a valid email address
com.example.UserRegistration.phone=Phone number must be in the format (123) 456-7890
Now, let's update the UserRegistration class to use localized messages. Remove all the hardcoded message attributes from the validators:
//@NotBlank(message = "Username is required")
@NotBlank
Let's create a French localization file as an example (resources/ValidationMessages_fr.properties):
jakarta.validation.constraints.NotBlank.message=Ce champ est obligatoire
jakarta.validation.constraints.Size.message=La taille doit être comprise entre {min} et {max} caractères
jakarta.validation.constraints.Email.message=Veuillez entrer une adresse email valide
com.example.UserRegistration.phone=Le numéro de téléphone doit être au format (123) 456-7890
Finally, let's update the application.properties to configure the default locale:
quarkus.default-locale=en
quarkus.locales=en,fr
You can test the localization with curl:
curl -X POST http://localhost:8080/register \
-H "Accept-Language: fr" \
-d "username=a&email=invalid&phone=1234567890"
Will show html with French error messages.
Take it further
Want to try integrating something else on your own?
Integrate CSRF protection using Quarkus security extensions.
Your First Line Of Defense
Validating user input is the first line of defense for any modern Java application. In Quarkus:
Jakarta Bean Validation makes input validation declarative and simple.
Qute allows building secure, user-friendly forms.
Server-side validation ensures client-side bypass attempts fail.
This article showed how to build a secure registration feature using Quarkus, Hibernate Validator, and Qute templates, closely following OWASP recommendations.