Skip to main content

input-validation

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

소스 정보

저장소
Hack23/cia
최근 소스 활동
2026년 2월 10일 22:35
감지된 SKILL.md 언어
영어
스타
239
포크
57

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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:** ```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) )); }
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기