| name | input-validation |
| description | Implement comprehensive input validation to prevent XSS, SQL injection, and command injection attacks with sanitization strategies |
| license | Apache-2.0 |
Input Validation Skill
Purpose
This skill provides systematic approaches to validate, sanitize, and encode all user inputs to prevent injection attacks, XSS, and data corruption. It implements defense-in-depth validation at multiple layers aligned with OWASP best practices and Hack23 ISMS secure coding standards.
When to Use This Skill
Apply this skill when:
- ✅ Processing any user-provided input (forms, APIs, URL parameters)
- ✅ Handling data from external systems (Riksdagen API, World Bank)
- ✅ Constructing database queries or commands
- ✅ Rendering user-generated content in UI
- ✅ Processing file uploads
- ✅ Implementing search functionality
- ✅ Building dynamic SQL, LDAP, or OS commands
Validation Principles
Principle #1: Validate Early, Validate Often
Multi-Layer Validation:
Client Side (JavaScript) → Basic UX validation
↓
Server Side (Controller) → Format and type validation
↓
Service Layer → Business logic validation
↓
Database Layer → Constraint enforcement
Principle #2: Allowlist Over Blocklist
❌ INSECURE - Blocklist Approach:
public boolean isValidInput(String input) {
String[] badPatterns = {"<script>", "javascript:", "onerror=", "' OR '1'='1"};
for (String pattern : badPatterns) {
if (input.toLowerCase().contains(pattern)) {
return false;
}
}
return true;
}
✅ SECURE - Allowlist Approach:
public boolean isValidPoliticianName(String name) {
return name != null &&
name.matches("^[A-Za-zÀ-ÿ\\s'-]{2,50}$") &&
name.length() >= 2 &&
name.length() <= 50;
}
public boolean isValidEmail(String email) {
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
return email != null && email.matches(emailRegex);
}
public boolean isValidYear(Integer year) {
int currentYear = Year.now().getValue();
return year != null &&
year >= 1900 &&
year <= currentYear;
}
Principle #3: Fail Securely
@ControllerAdvice
public class ValidationExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(ValidationExceptionHandler.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
log.warn("Validation failed: {}", ex.getBindingResult().getAllErrors());
ErrorResponse error = new ErrorResponse(
"Invalid input",
"One or more fields contain invalid data"
);
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolation(
ConstraintViolationException ex) {
log.warn("Constraint violation: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
"Invalid input",
"The provided data does not meet requirements"
);
return ResponseEntity.badRequest().body(error);
}
}
Input Validation Patterns
Pattern #1: Bean Validation (JSR-380)
@Entity
@Table(name = "politician")
public class Politician {
@Id
@NotNull
@Pattern(regexp = "^[0-9]{12}$", message = "Personal ID must be 12 digits")
private String personalId;
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be 2-50 characters")
@Pattern(regexp = "^[A-Za-zÀ-ÿ\\s'-]+$", message = "First name contains invalid characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(min = 2, max = 50, message = "Last name must be 2-50 characters")
@Pattern(regexp = "^[A-Za-zÀ-ÿ\\s'-]+$", message = "Last name contains invalid characters")
private String lastName;
@NotNull(message = "Birth date is required")
@Past(message = "Birth date must be in the past")
private LocalDate birthDate;
@Email(message = "Invalid email format")
@Pattern(regexp = "^[A-Za-z0-9+_.-]+@riksdagen\\.se$",
message = "Only riksdagen.se emails allowed")
private String officialEmail;
@Pattern(regexp = "^\\+46[0-9]{9}$", message = "Invalid Swedish phone number")
private String phoneNumber;
@NotNull(message = "Party is required")
@Enumerated(EnumType.STRING)
private PoliticalParty party;
}
public class CreatePoliticianRequest {
@NotBlank
@Size(min = 2, max = 50)
@Pattern(regexp = "^[A-Za-zÀ-ÿ\\s'-]+$")
String firstName;
String lastName;
LocalDate birthDate;
String email;
String phoneNumber;
String partyCode;
}
{
ResponseEntity<Politician> {
politicianService.create(request);
ResponseEntity.status(HttpStatus.CREATED).body(politician);
}
ResponseEntity<Politician> {
politicianService.findById(id)
.orElseThrow(() -> ());
ResponseEntity.ok(politician);
}
}
Pattern #2: Custom Validators
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SwedishPersonalIdValidator.class)
public @interface ValidSwedishPersonalId {
String message() default "Invalid Swedish personal ID";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SwedishPersonalIdValidator implements ConstraintValidator<ValidSwedishPersonalId, String> {
@Override
public boolean isValid(String personalId, ConstraintValidatorContext context) {
if (personalId == null || personalId.length() != 12) {
return false;
}
if (!personalId.matches("^[0-9]{12}$")) {
return false;
}
try {
int year = Integer.parseInt(personalId.substring(0, 4));
int month = Integer.parseInt(personalId.substring(, ));
Integer.parseInt(personalId.substring(, ));
LocalDate.of(year, month, day);
} (DateTimeException e) {
;
}
validateLuhnChecksum(personalId);
}
{
;
;
( number.length() - ; i >= ; i--) {
Character.getNumericValue(number.charAt(i));
(alternate) {
digit *= ;
(digit > ) {
digit = (digit % ) + ;
}
}
sum += digit;
alternate = !alternate;
}
(sum % == );
}
}
{
String personalId;
}
Pattern #3: Sanitization for XSS Prevention
@Component
public class InputSanitizer {
private final Policy antiSamyPolicy;
public InputSanitizer() throws PolicyException {
this.antiSamyPolicy = Policy.getInstance(
getClass().getResourceAsStream("/antisamy-policy.xml")
);
}
public String sanitizeHtml(String input) {
if (input == null) return null;
try {
AntiSamy antiSamy = new AntiSamy();
CleanResults results = antiSamy.scan(input, antiSamyPolicy);
if (results.getNumberOfErrors() > 0) {
log.warn("Sanitized HTML input. Errors: {}",
results.getErrorMessages());
}
return results.getCleanHTML();
} catch (Exception e) {
log.error("Error sanitizing HTML", e);
return "";
}
}
public String encodeForHtml(String input) {
(input == ) ;
StringEscapeUtils.escapeHtml4(input);
}
String {
(input == ) ;
StringEscapeUtils.escapeEcmaScript(input);
}
String {
(input == ) ;
{
URLEncoder.encode(input, StandardCharsets.UTF_8);
} (Exception e) {
;
}
}
String {
(input == ) ;
input.replaceAll(, );
}
String {
(input == ) ;
(input.length() <= maxLength) input;
input.substring(, maxLength);
}
}
{
InputSanitizer sanitizer;
Comment {
sanitizer.sanitizeHtml(content);
sanitizedContent = sanitizer.truncate(sanitizedContent, );
();
comment.setAuthorId(authorId);
comment.setContent(sanitizedContent);
comment.setCreatedAt(Instant.now());
commentRepository.save(comment);
}
}
{
InputSanitizer sanitizer;
{
sanitizer.encodeForHtml(comment.getContent());
( + safeContent + );
add(contentHtml);
}
}
Pattern #4: SQL Injection Prevention
✅ SECURE - Parameterized Queries:
@Repository
public interface PoliticianRepository extends JpaRepository<Politician, String> {
@Query("SELECT p FROM Politician p WHERE " +
"LOWER(p.firstName) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR " +
"LOWER(p.lastName) LIKE LOWER(CONCAT('%', :searchTerm, '%'))")
List<Politician> search(@Param("searchTerm") String searchTerm);
List<Politician> findByPartyAndDistrictOrderByLastNameAsc(String party, String district);
}
@Repository
public class PoliticianSearchRepository {
@PersistenceContext
private EntityManager entityManager;
public List<Politician> advancedSearch(PoliticianSearchCriteria criteria) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Politician> query = cb.createQuery(Politician.class);
Root<Politician> politician = query.from(Politician.class);
List<Predicate> predicates = new ArrayList<>();
if (criteria.getFirstName() != null) {
predicates.add(cb.like(
cb.lower(politician.get("firstName")),
"%" + criteria.getFirstName().toLowerCase() + "%"
));
}
if (criteria.getParty() != null) {
predicates.add(cb.equal(politician.get(), criteria.getParty()));
}
(criteria.getMinBirthYear() != ) {
predicates.add(cb.greaterThanOrEqualTo(
politician.get(),
LocalDate.of(criteria.getMinBirthYear(), , )
));
}
query.where(predicates.toArray( []));
query.orderBy(cb.asc(politician.get()));
entityManager.createQuery(query).getResultList();
}
}
❌ INSECURE - String Concatenation:
public List<Politician> searchUnsafe(String searchTerm) {
String sql = "SELECT * FROM politician WHERE " +
"first_name LIKE '%" + searchTerm + "%'";
return jdbcTemplate.query(sql, new PoliticianRowMapper());
}
Pattern #5: File Upload Validation
@Configuration
public class FileUploadConfig {
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
private static final Set<String> ALLOWED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "pdf");
private static final Set<String> ALLOWED_MIME_TYPES = Set.of(
"image/jpeg", "image/png", "application/pdf"
);
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofBytes(MAX_FILE_SIZE));
factory.setMaxRequestSize(DataSize.ofBytes(MAX_FILE_SIZE));
return factory.createMultipartConfig();
}
}
@Service
public class FileValidationService {
private static final Logger log = LoggerFactory.getLogger(FileValidationService.class);
private static Set<String> ALLOWED_EXTENSIONS = Set.of(, , , );
* * ;
ValidationException {
(file == || file.isEmpty()) {
();
}
(file.getSize() > MAX_FILE_SIZE) {
();
}
file.getOriginalFilename();
(originalFilename == || originalFilename.contains()) {
();
}
getFileExtension(originalFilename).toLowerCase();
(!ALLOWED_EXTENSIONS.contains(extension)) {
(
+ ALLOWED_EXTENSIONS);
}
{
detectContentType(file.getInputStream());
(!isAllowedContentType(contentType)) {
();
}
} (IOException e) {
();
}
scanForMalware(file);
}
String {
filename.lastIndexOf();
(lastDot == -) ;
filename.substring(lastDot + );
}
String IOException {
URLConnection.guessContentTypeFromStream(inputStream);
}
{
Set<String> allowed = Set.of(, , );
contentType != && allowed.contains(contentType);
}
{
}
String {
getFileExtension(originalFilename);
UUID.randomUUID().toString();
uuid + + extension;
}
}
{
FileValidationService fileValidationService;
ResponseEntity<UploadResponse> {
fileValidationService.validateFile(file);
fileValidationService.generateSafeFilename(
file.getOriginalFilename()
);
Paths.get(, safeFilename);
Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
politicianService.updatePhoto(politicianId, safeFilename);
(safeFilename, file.getSize());
ResponseEntity.ok(response);
}
}
Context-Specific Encoding
HTML Context
<div th:text="${politician.firstName}"></div> <!-- Auto-escaped -->
String safe = StringEscapeUtils.escapeHtml4(userInput);
JavaScript Context
String safeJs = StringEscapeUtils.escapeEcmaScript(userInput);
String json = objectMapper.writeValueAsString(data);
URL Context
String safeUrl = URLEncoder.encode(searchTerm, StandardCharsets.UTF_8);
String url = "/search?q=" + safeUrl;
SQL Context
Input Validation Checklist
Before deploying code that processes user input:
Data Type Validation:
- ✅ Verify data type (string, int, date, etc.)
- ✅ Check for null/empty values
- ✅ Validate length constraints
- ✅ Ensure proper encoding (UTF-8)
Format Validation:
- ✅ Use regex for pattern matching
- ✅ Validate against allowlist of acceptable values
- ✅ Check date/time formats
- ✅ Verify email/phone formats
Business Logic Validation:
- ✅ Check value ranges (min/max)
- ✅ Validate relationships between fields
- ✅ Verify uniqueness constraints
- ✅ Ensure data consistency
Security Validation:
- ✅ Sanitize HTML content (XSS prevention)
- ✅ Use parameterized queries (SQL injection prevention)
- ✅ Validate file uploads (type, size, content)
- ✅ Check for path traversal attempts
- ✅ Prevent command injection
Output Encoding:
- ✅ Encode for HTML context
- ✅ Encode for JavaScript context
- ✅ Encode for URL context
- ✅ Encode for CSS context
ISMS Compliance Mapping
ISO 27001:2022 Controls
- A.8.28 - Secure Coding: Input validation as secure coding practice
- A.14.2.1 - Secure Development Policy: Validation requirements
- A.8.7 - Protection Against Malware: File upload validation
NIST Cybersecurity Framework
- PR.DS-5: Protections against data leaks implemented
- DE.CM-4: Malicious input detected
CIS Controls v8
- Control 16.11: Establish input validation processes
- Control 18.5: Validate application input
Hack23 ISMS Policy References
Input Validation & Secure Coding Framework:
All Hack23 ISMS Policies: https://github.com/Hack23/ISMS-PUBLIC
CIA Platform Architecture References
References
Standards & Guidelines
Tools & Libraries