Skip to main content

Spring Boot Security Implementation

Spring Boot-specific implementations for application security using Spring Security, Jakarta Bean Validation, and framework security features.

Overview​

This guide covers Spring Boot-specific security implementations. For security concepts and principles, see the Security Overview. For detailed explanations of vulnerabilities like SQL injection and XSS, see Input Validation.

Core Focus Areas:

  • Spring Security configuration
  • Jakarta Bean Validation integration
  • Spring-specific security patterns
  • Framework security features
Related Security Topics

Spring Boot Input Validation​

Spring Boot integrates Jakarta Bean Validation (JSR-380) for declarative input validation. For validation concepts and attack explanations, see Input Validation.

Jakarta Bean Validation Setup​

Spring Boot includes Jakarta Bean Validation for declarative validation:

build.gradle:

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

Basic Validation:

public record PaymentRequest(
@NotBlank(message = "Customer ID is required")
@Size(max = 50, message = "Customer ID too long")
String customerId,

@NotNull(message = "Amount is required")
@DecimalMin(value = "0.01", message = "Amount must be positive")
@DecimalMax(value = "999999.99", message = "Amount exceeds maximum")
@Digits(integer = 6, fraction = 2, message = "Invalid amount format")
BigDecimal amount,

@NotBlank(message = "Currency is required")
@Pattern(regexp = "^[A-Z]{3}$", message = "Currency must be 3 uppercase letters")
String currency,

@Size(max = 500, message = "Description too long")
String description
) {}

What each annotation does:

  • @NotBlank: Ensures string is not null, empty, or whitespace-only
  • @NotNull: Ensures value is present (for non-strings)
  • @Size: Limits string length (prevents buffer overflow, database errors)
  • @Pattern: Validates format with regex (e.g., currency codes, phone numbers)
  • @DecimalMin/@DecimalMax: Range checking for numbers
  • @Digits: Validates decimal precision (integer + fraction digits)

Enable validation in controller:

@RestController
@RequestMapping("/api/v1/payments")
@Validated // Enables validation on this controller
public class PaymentController {

@PostMapping
public PaymentResponse createPayment(@Valid @RequestBody PaymentRequest request) {
// If validation fails, Spring throws MethodArgumentNotValidException
// Handle this in global exception handler
return paymentService.createPayment(request);
}
}

Why @Valid matters: Without @Valid, Spring ignores validation annotations on the request object. The @Valid annotation triggers the validation framework to check constraints before the method executes.

Custom Validation​

Some validation rules can't be expressed with annotations:

@Component
public class PaymentValidator {

public void validate(PaymentRequest request) {
// Business rule: USD payments must be at least $1
if ("USD".equals(request.currency())
&& request.amount().compareTo(new BigDecimal("1.00")) < 0) {
throw new ValidationException("USD payments must be at least $1.00");
}

// Business rule: Maximum daily transaction limit
if (request.amount().compareTo(new BigDecimal("10000.00")) > 0) {
// This requires additional validation - check customer's daily total
validateDailyLimit(request.customerId(), request.amount());
}

// Check for suspicious patterns
if (isSuspicious(request.description())) {
log.warn("Suspicious payment description: customerId={}", request.customerId());
// Don't reject immediately - flag for review
flagForReview(request);
}
}

private boolean isSuspicious(String description) {
if (description == null) return false;

// Look for patterns indicating potential fraud
String lower = description.toLowerCase();
return lower.contains("test")
|| lower.contains("fraud")
|| lower.matches(".*<script.*") // Basic XSS attempt
|| lower.contains("../"); // Path traversal attempt
}
}

Why separate validator class:

  • Testability: Easy to unit test validation logic
  • Reusability: Use same validator for API, batch jobs, message handlers
  • Clarity: Complex validation doesn't clutter controller

Use in service:

@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentValidator validator;
private final PaymentRepository repository;

public Payment createPayment(PaymentRequest request) {
// Bean Validation already passed (from @Valid in controller)
// Now apply business rules
validator.validate(request);

// Proceed with business logic
var payment = buildPayment(request);
return repository.save(payment);
}
}

SQL Injection Prevention in Spring Boot​

Spring Boot's data access frameworks (Spring Data JPA, JdbcTemplate) prevent SQL injection through parameterized queries. For detailed explanation of SQL injection attacks, see Input Validation - SQL Injection.

Safe Patterns with Spring Data JPA​

Spring Data JPA (safe by default):

@Repository
public interface PaymentRepository extends JpaRepository<Payment, String> {
// Parameters automatically escaped
List<Payment> findByCustomerId(String customerId);

// Named parameters are safe
@Query("SELECT p FROM Payment p WHERE p.customerId = :customerId")
List<Payment> findPayments(@Param("customerId") String customerId);
}

JdbcTemplate (use placeholders):

//  GOOD: SAFE: Parameterized query
public List<Payment> findByCustomerId(String customerId) {
String sql = "SELECT * FROM payments WHERE customer_id = ?";
return jdbcTemplate.query(sql, paymentRowMapper, customerId);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
// Parameter automatically escaped
}

// GOOD: SAFE: Named parameters
public List<Payment> findByCustomerIdAndStatus(String customerId, PaymentStatus status) {
String sql = "SELECT * FROM payments WHERE customer_id = :customerId AND status = :status";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("customerId", customerId)
.addValue("status", status.name());

return namedParameterJdbcTemplate.query(sql, params, paymentRowMapper);
}

Why this works: The database driver treats parameters as data, not SQL code. Even if user input contains SQL syntax, it's escaped and treated as a literal string value.

Dynamic queries (when query structure changes):

public List<Payment> searchPayments(PaymentSearchCriteria criteria) {
StringBuilder sql = new StringBuilder("SELECT * FROM payments WHERE 1=1");
MapSqlParameterSource params = new MapSqlParameterSource();

// Build query dynamically but use parameters
if (criteria.customerId() != null) {
sql.append(" AND customer_id = :customerId");
params.addValue("customerId", criteria.customerId());
}

if (criteria.minAmount() != null) {
sql.append(" AND amount >= :minAmount");
params.addValue("minAmount", criteria.minAmount());
}

if (criteria.status() != null) {
sql.append(" AND status = :status");
params.addValue("status", criteria.status().name());
}

return namedParameterJdbcTemplate.query(sql.toString(), params, paymentRowMapper);
}

Key points:

  • SQL structure is built safely (no user input in string concatenation)
  • User values are always passed as parameters
  • Database handles escaping automatically

XSS Prevention in Spring Boot​

Spring Boot's JSON serialization automatically escapes special characters. For detailed XSS attack explanations and frontend prevention, see Input Validation - XSS Prevention.

Automatic JSON Escaping​

For REST APIs returning JSON, Spring Boot handles encoding automatically:

@RestController
public class PaymentController {

@GetMapping("/{id}")
public PaymentResponse getPayment(@PathVariable String id) {
return paymentService.getPayment(id);
// Spring's JSON serializer automatically escapes special characters
// "<script>" becomes "\\u003Cscript\\u003E" in JSON
}
}

For HTML responses (if you render HTML from Spring Boot):

// Thymeleaf automatically escapes by default
<p th:text="${payment.description}">Description</p>
// Even if description contains <script>, it's rendered as text

// To render raw HTML (DANGEROUS - only for trusted content):
<p th:utext="${trustedContent}">Content</p>

Input Sanitization​

For fields that accept rich text, sanitize on input:

@Component
public class HtmlSanitizer {

private final PolicyFactory policy;

public HtmlSanitizer() {
// Define allowed HTML tags and attributes
this.policy = new HtmlPolicyBuilder()
.allowElements("p", "br", "strong", "em", "ul", "ol", "li")
.allowAttributes("class").onElements("p")
.toFactory();
}

public String sanitize(String input) {
if (input == null) return null;
// Removes any disallowed HTML, keeps safe tags
return policy.sanitize(input);
}
}

// Usage
@Service
@RequiredArgsConstructor
public class CommentService {
private final HtmlSanitizer sanitizer;

public Comment createComment(String userId, String text) {
// Strip dangerous HTML before saving
String safeText = sanitizer.sanitize(text);
return commentRepository.save(new Comment(userId, safeText));
}
}

build.gradle (for OWASP Java HTML Sanitizer):

dependencies {
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
}

Sensitive Data Protection in Spring Boot​

For general data protection principles, see Data Protection. This section covers Spring-specific encryption utilities.

Spring Security Crypto for Encryption​

For sensitive data at rest, use Spring's encryption support:

@Configuration
public class EncryptionConfig {

@Bean
public TextEncryptor textEncryptor() {
// Get encryption key from environment (not hardcoded!)
String salt = System.getenv("ENCRYPTION_SALT");
String secret = System.getenv("ENCRYPTION_SECRET");

return Encryptors.text(secret, salt);
}
}

@Service
@RequiredArgsConstructor
public class CustomerService {
private final TextEncryptor encryptor;
private final CustomerRepository repository;

public Customer createCustomer(CustomerRequest request) {
// Encrypt SSN before saving
String encryptedSsn = encryptor.encrypt(request.ssn());

var customer = new Customer();
customer.setName(request.name());
customer.setEncryptedSsn(encryptedSsn); // Store encrypted

return repository.save(customer);
}

public String getCustomerSsn(String customerId) {
var customer = repository.findById(customerId)
.orElseThrow(() -> new NotFoundException("Customer not found"));

// Decrypt when needed
return encryptor.decrypt(customer.getEncryptedSsn());
}
}

build.gradle:

dependencies {
implementation 'org.springframework.security:spring-security-crypto'
}

Secure Error Handling with Spring Boot​

Spring Boot's error handling can inadvertently expose sensitive information. Configure secure error responses using @RestControllerAdvice.

Secure Exception Handling​

@RestControllerAdvice
public class SecureExceptionHandler {

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ProblemDetail handleGenericError(Exception ex) {
// Log full details internally for debugging
log.error("Payment processing error", ex);

// Return generic message to client
return ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"An error occurred processing your request"
);
}

@ExceptionHandler(PaymentNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ProblemDetail handleNotFound(PaymentNotFoundException ex) {
// Generic message - don't expose if resource exists
return ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
"Payment not found"
);
}
}

Production Error Configuration​

# application-prod.yml
server:
error:
include-message: never # Don't expose exception messages
include-stacktrace: never # Never include stack traces
include-binding-errors: never # Don't expose validation details

Path Traversal Prevention in Spring Boot​

For path traversal attack explanations, see Input Validation. This section shows Spring Boot-specific file handling patterns.

Secure File Service Pattern​

@Service
public class FileService {

private static final Path BASE_DIRECTORY = Paths.get("/app/files").toAbsolutePath();

public byte[] getFile(String filename) throws IOException {
// Validate filename doesn't contain path separators
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
throw new SecurityException("Invalid filename");
}

// Build path and normalize
Path filePath = BASE_DIRECTORY.resolve(filename).normalize();

// Verify final path is still within BASE_DIRECTORY
if (!filePath.startsWith(BASE_DIRECTORY)) {
throw new SecurityException("Path traversal attempt detected");
}

// Additional check: file must exist and be a regular file
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new FileNotFoundException("File not found");
}

return Files.readAllBytes(filePath);
}
}

What's happening:

  • Reject filenames with ., /, \ (path separators)
  • normalize() resolves . and .. in path
  • Verify final path still starts with base directory
  • Check file exists and is a regular file (not directory)

Mass Assignment Prevention with DTOs​

Mass assignment vulnerabilities occur when binding request data directly to entities. For mass assignment attack explanations, see Input Validation. Spring Boot prevents this using DTOs (Data Transfer Objects).

DTO Pattern​

// Request DTO - only fields user can set
public record CreateUserRequest(
@NotBlank String name,
@Email String email
// No isAdmin field - user can't set it
) {}

// Service sets internal fields
@Service
@RequiredArgsConstructor
public class UserService {

public User createUser(CreateUserRequest request) {
var user = new User();
user.setId(UUID.randomUUID().toString());
user.setName(request.name());
user.setEmail(request.email());
user.setIsAdmin(false); // Controlled internally, not from user input

return userRepository.save(user);
}

// Only admins can promote users
@PreAuthorize("hasRole('ADMIN')")
public User promoteToAdmin(String userId) {
var user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User not found"));

user.setIsAdmin(true);
return userRepository.save(user);
}
}

Spring Security Configuration​

Spring Security provides comprehensive security features including authentication, authorization, and protection against common attacks.

SecurityFilterChain Configuration​

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
.frameOptions(frame -> frame.deny())
// X-XSS-Protection header omitted: removed from Spring Security 6.1+
// and dropped by modern browsers. Rely on Content-Security-Policy instead.
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// For JWT/OAuth2 REST APIs: use STATELESS (no server-side sessions)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);

return http.build();
}
}

Key Security Headers:

  • CSRF Token: Prevents cross-site request forgery (see Input Validation - CSRF)
  • Content Security Policy: Mitigates XSS attacks
  • X-Frame-Options: Prevents clickjacking

Summary​

Spring Boot Security Checklist:

  1. Jakarta Bean Validation: Use @Valid with @NotNull, @Size, @Pattern annotations
  2. Spring Data JPA: Automatically prevents SQL injection with parameterized queries
  3. JdbcTemplate: Use ? placeholders or named parameters, never string concatenation
  4. JSON Serialization: Spring Boot automatically escapes output
  5. DTOs: Prevent mass assignment by using dedicated request/response objects
  6. Spring Security Crypto: Use TextEncryptor for sensitive data encryption
  7. Error Handling: Configure @RestControllerAdvice to hide internal details
  8. SecurityFilterChain: Enable CSRF, CSP, frame options
  9. Production Config: Disable stack traces and error details in application-prod.yml
Related Documentation