skilly. Buy ad slot
All skills
Uncategorized / AGENT SKILL

input-validation

Hack23/cia
0 installs 241 GitHub stars
0

Implement comprehensive input validation to prevent XSS, SQL injection, and command injection attacks with sanitization strategies

Low community score

input-validation: This skill has a community score below −5, meaning it has received more downvotes than upvotes. Review its source and assessment before installing. Community votes are not a security review.

BEFORE YOU INSTALL

Understand the trade-offs.

SECURITY REVIEW

Not yet assessed

Review the original instructions and requested permissions before installing.

No security review is available for this catalog entry yet.

SKILL QUALITY

Not yet assessed

How clearly the skill guides your agent, how complete its workflow is, and how you can check the outcome.

No quality assessment is available for this catalog entry yet.

The full skill.

Original instructions from the publisher’s SKILL.md

# 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:**
```java
// BAD: Trying to block malicious patterns (incomplete, bypassable)
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:**
```java
// GOOD: Define what IS valid
public boolean isValidPoliticianName(String name) {
    // Only allow letters, spaces, hyphens, and apostrophes
    return name != null && 
           name.matches("^[A-Za-zÀ-ÿ\\s'-]{2,50}$") &&
           name.length() >= 2 &&
           name.length() <= 50;
}

public boolean isValidEmail(String email) {
    // Use standard email regex
    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) {
    // Define valid range
    int currentYear = Year.now().getValue();
    return year != null && 
           year >= 1900 && 
           year <= currentYear;
}
```

### Principle #3: Fail Securely

```java
@ControllerAdvice
public class ValidationExceptionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(ValidationExceptionHandler.class);
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException ex) {
        
        // Log detailed error for debugging (but don't expose to user)
        log.warn("Validation failed: {}", ex.getBindingResult().getAllErrors());
        
        // Return generic error to user (don't leak validation logic)
        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)

```java
// Domain model with validation constraints
@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;
}

// DTO for API requests
public class CreatePoliticianRequest {
    
    @NotBlank
    @Size(min = 2, max = 50)
    @Pattern(regexp = "^[A-Za-zÀ-ÿ\\s'-]+$")
    private String firstName;
    
    @NotBlank
    @Size(min = 2, max = 50)
    @Pattern(regexp = "^[A-Za-zÀ-ÿ\\s'-]+$")
    private String lastName;
    
    @NotNull
    @Past
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate birthDate;
    
    @Email
    private String email;
    
    @Pattern(regexp = "^\\+46[0-9]{9}$")
    private String phoneNumber;
    
    @NotNull
    private String partyCode;
    
    // Getters and setters
}

// Controller with validation
@RestController
@RequestMapping("/api/politicians")
@Validated
public class PoliticianController {
    
    @PostMapping
    public ResponseEntity<Politician> createPolitician(
            @Valid @RequestBody CreatePoliticianRequest request) {
        
        // If validation passes, create politician
        Politician politician = politicianService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(politician);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Politician> getPolitician(
            @PathVariable 
            @Pattern(regexp = "^[0-9]{12}$", message = "Invalid politician ID") 
            String id) {
        
        Politician politician = politicianService.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Politician not found"));
        
        return ResponseEntity.ok(politician);
    }
}
```

### Pattern #2: Custom Validators

```java
// Custom annotation for Swedish Personal ID
@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 {};
}

// Validator implementation with checksum
public class SwedishPersonalIdValidator implements ConstraintValidator<ValidSwedishPersonalId, String> {
    
    @Override
    public boolean isValid(String personalId, ConstraintValidatorContext context) {
        if (personalId == null || personalId.length() != 12) {
            return false;
        }
        
        // Check format: YYYYMMDDNNNN
        if (!personalId.matches("^[0-9]{12}$")) {
            return false;
        }
        
        // Validate date part
        try {
            int year = Integer.parseInt(personalId.substring(0, 4));
            int month = Integer.parseInt(personalId.substring(4, 6));
            int day = Integer.parseInt(personalId.substring(6, 8));
            
            LocalDate.of(year, month, day);
        } catch (DateTimeException e) {
            return false;
        }
        
        // Validate Luhn checksum
        return validateLuhnChecksum(personalId);
    }
    
    private boolean validateLuhnChecksum(String number) {
        int sum = 0;
        boolean alternate = false;
        
        for (int i = number.length() - 1; i >= 0; i--) {
            int digit = Character.getNumericValue(number.charAt(i));
            
            if (alternate) {
                digit *= 2;
                if (digit > 9) {
                    digit = (digit % 10) + 1;
                }
            }
            
            sum += digit;
            alternate = !alternate;
        }
        
        return (sum % 10 == 0);
    }
}

// Usage
@Entity
public class Politician {
    @Id
    @ValidSwedishPersonalId
    private String personalId;
}
```

### Pattern #3: Sanitization for XSS Prevention

```java
@Component
public class InputSanitizer {
    
    private final Policy antiSamyPolicy;
    
    public InputSanitizer() throws PolicyException {
        // Load OWASP AntiSamy policy
        this.antiSamyPolicy = Policy.getInstance(
            getClass().getResourceAsStream("/antisamy-policy.xml")
        );
    }
    
    /**
     * Sanitize HTML input to prevent XSS
     */
    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);
            // Fail securely - return empty string
            return "";
        }
    }
    
    /**
     * Encode for HTML output
     */
    public String encodeForHtml(String input) {
        if (input == null) return null;
        return StringEscapeUtils.escapeHtml4(input);
    }
    
    /**
     * Encode for JavaScript output
     */
    public String encodeForJavaScript(String input) {
        if (input == null) return null;
        return StringEscapeUtils.escapeEcmaScript(input);
    }
    
    /**
     * Encode for URL parameter
     */
    public String encodeForUrl(String input) {
        if (input == null) return null;
        try {
            return URLEncoder.encode(input, StandardCharsets.UTF_8);
        } catch (Exception e) {
            return "";
        }
    }
    
    /**
     * Remove all non-alphanumeric characters
     */
    public String alphanumericOnly(String input) {
        if (input == null) return null;
        return input.replaceAll("[^A-Za-z0-9]", "");
    }
    
    /**
     * Truncate to maximum length
     */
    public String truncate(String input, int maxLength) {
        if (input == null) return null;
        if (input.length() <= maxLength) return input;
        return input.substring(0, maxLength);
    }
}

// Usage in service layer
@Service
public class CommentService {
    
    @Autowired
    private InputSanitizer sanitizer;
    
    public Comment createComment(String authorId, String content) {
        // Sanitize user-generated content
        String sanitizedContent = sanitizer.sanitizeHtml(content);
        
        // Truncate to reasonable length
        sanitizedContent = sanitizer.truncate(sanitizedContent, 5000);
        
        Comment comment = new Comment();
        comment.setAuthorId(authorId);
        comment.setContent(sanitizedContent);
        comment.setCreatedAt(Instant.now());
        
        return commentRepository.save(comment);
    }
}

// Usage in Vaadin UI
@Component
public class CommentView extends VerticalLayout {
    
    @Autowired
    private InputSanitizer sanitizer;
    
    public void displayComment(Comment comment) {
        // Encode for HTML display
        String safeContent = sanitizer.encodeForHtml(comment.getContent());
        
        Html contentHtml = new Html("<div>" + safeContent + "</div>");
        add(contentHtml);
    }
}
```

### Pattern #4: SQL Injection Prevention

✅ **SECURE - Parameterized Queries:**
```java
@Repository
public interface PoliticianRepository extends JpaRepository<Politician, String> {
    
    // JPA Query with named parameters (SAFE)
    @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);
    
    // Spring Data query methods (SAFE)
    List<Politician> findByPartyAndDistrictOrderByLastNameAsc(String party, String district);
}

// Criteria API for dynamic queries (SAFE)
@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<>();
        
        // Safe parameter binding
        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("party"), criteria.getParty()));
        }
        
        if (criteria.getMinBirthYear() != null) {
            predicates.add(cb.greaterThanOrEqualTo(
                politician.get("birthDate"),
                LocalDate.of(criteria.getMinBirthYear(), 1, 1)
            ));
        }
        
        query.where(predicates.toArray(new Predicate[0]));
        query.orderBy(cb.asc(politician.get("lastName")));
        
        return entityManager.createQuery(query).getResultList();
    }
}
```

❌ **INSECURE - String Concatenation:**
```java
// NEVER DO THIS!
public List<Politician> searchUnsafe(String searchTerm) {
    String sql = "SELECT * FROM politician WHERE " +
                 "first_name LIKE '%" + searchTerm + "%'";  // SQL INJECTION!
    
    return jdbcTemplate.query(sql, new PoliticianRowMapper());
}
```

### Pattern #5: File Upload Validation

```java
@Configuration
public class FileUploadConfig {
    
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
    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 final Set<String> ALLOWED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "pdf");
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
    
    /**
     * Comprehensive file validation
     */
    public void validateFile(MultipartFile file) throws ValidationException {
        if (file == null || file.isEmpty()) {
            throw new ValidationException("File is required");
        }
        
        // 1. Check file size
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new ValidationException("File size exceeds maximum of 10 MB");
        }
        
        // 2. Validate filename
        String originalFilename = file.getOriginalFilename();
        if (originalFilename == null || originalFilename.contains("..")) {
            throw new ValidationException("Invalid filename");
        }
        
        // 3. Check file extension
        String extension = getFileExtension(originalFilename).toLowerCase();
        if (!ALLOWED_EXTENSIONS.contains(extension)) {
            throw new ValidationException(
                "File type not allowed. Allowed types: " + ALLOWED_EXTENSIONS);
        }
        
        // 4. Verify MIME type from content (not just filename)
        try {
            String contentType = detectContentType(file.getInputStream());
            if (!isAllowedContentType(contentType)) {
                throw new ValidationException("File content type not allowed");
            }
        } catch (IOException e) {
            throw new ValidationException("Error reading file");
        }
        
        // 5. Scan for malware (if applicable)
        scanForMalware(file);
    }
    
    private String getFileExtension(String filename) {
        int lastDot = filename.lastIndexOf('.');
        if (lastDot == -1) return "";
        return filename.substring(lastDot + 1);
    }
    
    private String detectContentType(InputStream inputStream) throws IOException {
        return URLConnection.guessContentTypeFromStream(inputStream);
    }
    
    private boolean isAllowedContentType(String contentType) {
        Set<String> allowed = Set.of("image/jpeg", "image/png", "application/pdf");
        return contentType != null && allowed.contains(contentType);
    }
    
    private void scanForMalware(MultipartFile file) {
        // Integrate with malware scanning service if available
        // E.g., ClamAV, VirusTotal API
    }
    
    /**
     * Generate safe filename
     */
    public String generateSafeFilename(String originalFilename) {
        String extension = getFileExtension(originalFilename);
        String uuid = UUID.randomUUID().toString();
        return uuid + "." + extension;
    }
}

@RestController
@RequestMapping("/api/uploads")
public class FileUploadController {
    
    @Autowired
    private FileValidationService fileValidationService;
    
    @PostMapping("/politician-photo")
    public ResponseEntity<UploadResponse> uploadPhoto(
            @RequestParam("file") MultipartFile file,
            @RequestParam("politicianId") @Pattern(regexp = "^[0-9]{12}$") String politicianId) {
        
        // Validate file
        fileValidationService.validateFile(file);
        
        // Generate safe filename
        String safeFilename = fileValidationService.generateSafeFilename(
            file.getOriginalFilename()
        );
        
        // Store file securely
        Path uploadPath = Paths.get("/secure/uploads/photos/", safeFilename);
        Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
        
        // Update database
        politicianService.updatePhoto(politicianId, safeFilename);
        
        UploadResponse response = new UploadResponse(safeFilename, file.getSize());
        return ResponseEntity.ok(response);
    }
}
```

## Context-Specific Encoding

### HTML Context
```java
// Vaadin/Thymeleaf - Automatic escaping
<div th:text="${politician.firstName}"></div>  <!-- Auto-escaped -->

// Manual encoding when needed
String safe = StringEscapeUtils.escapeHtml4(userInput);
```

### JavaScript Context
```java
// In Vaadin or JavaScript
String safeJs = StringEscapeUtils.escapeEcmaScript(userInput);
String json = objectMapper.writeValueAsString(data); // Proper JSON encoding
```

### URL Context
```java
// URL parameters
String safeUrl = URLEncoder.encode(searchTerm, StandardCharsets.UTF_8);
String url = "/search?q=" + safeUrl;
```

### SQL Context
```java
// ALWAYS use parameterized queries (covered earlier)
// NEVER build SQL strings with user input
```

## 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:**
- [Secure Development Policy](https://github.com/Hack23/ISMS-PUBLIC/blob/main/Secure_Development_Policy.md) - Comprehensive input validation requirements and OWASP Top 10 mitigation
- [Information Security Policy](https://github.com/Hack23/ISMS-PUBLIC/blob/main/Information_Security_Policy.md) - Security principles and defense-in-depth
- [Data Classification Policy](https://github.com/Hack23/ISMS-PUBLIC/blob/main/Data_Classification_Policy.md) - Data protection requirements
- [Threat Modeling](https://github.com/Hack23/ISMS-PUBLIC/blob/main/Threat_Modeling.md) - Input attack vectors and STRIDE analysis
- [Vulnerability Management](https://github.com/Hack23/ISMS-PUBLIC/blob/main/Vulnerability_Management.md) - Input validation vulnerability lifecycle

**All Hack23 ISMS Policies**: https://github.com/Hack23/ISMS-PUBLIC

## CIA Platform Architecture References

- **Security Architecture**: [CIA SECURITY_ARCHITECTURE.md](https://github.com/Hack23/cia/blob/master/SECURITY_ARCHITECTURE.md) - Input validation implementation
- **Threat Model**: [CIA THREAT_MODEL.md](https://github.com/Hack23/cia/blob/master/THREAT_MODEL.md) - Input-based attack scenarios

## References

### Standards & Guidelines
- **OWASP Input Validation**: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- **OWASP XSS Prevention**: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- **OWASP SQL Injection Prevention**: https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- **ISO 27001:2022 A.8.28**: Secure coding requirements
- **CWE-20**: Improper Input Validation

### Tools & Libraries
- **Bean Validation (JSR-380)**: https://beanvalidation.org/
- **OWASP AntiSamy**: https://owasp.org/www-project-antisamy/
- **ESAPI**: https://owasp.org/www-project-enterprise-security-api/