diff --git a/src/main/java/com/iemr/common/controller/sms/SMSController.java b/src/main/java/com/iemr/common/controller/sms/SMSController.java index ee985947..9bacf5b1 100644 --- a/src/main/java/com/iemr/common/controller/sms/SMSController.java +++ b/src/main/java/com/iemr/common/controller/sms/SMSController.java @@ -24,6 +24,7 @@ import java.util.Arrays; import javax.ws.rs.core.MediaType; +import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,7 +102,7 @@ public String getFullSMSTemplate( @Operation(summary = "Save SMS template") @PostMapping(value = "/saveSMSTemplate", produces = MediaType.APPLICATION_JSON, headers = "Authorization") public String saveSMSTemplate( - @Param(value = "\"{\\\"createdBy\\\":\\\"String\\\",\\\"providerServiceMapID\\\":\\\"String\\\",\\\"smsParameterMaps\\\":\\\"String\\\",\\\"smsTemplate\\\":\\\"String\\\",\\\"smsTemplateName\\\":\\\"String\\\",\\\"smsTypeID\\\":\\\"Integer\\\"}\"") @RequestBody CreateSMSRequest request, + @Param(value = "\"{\\\"createdBy\\\":\\\"String\\\",\\\"providerServiceMapID\\\":\\\"String\\\",\\\"smsParameterMaps\\\":\\\"String\\\",\\\"smsTemplate\\\":\\\"String\\\",\\\"smsTemplateName\\\":\\\"String\\\",\\\"smsTypeID\\\":\\\"Integer\\\"}\"") @Valid @RequestBody CreateSMSRequest request, HttpServletRequest serverRequest) { OutputResponse response = new OutputResponse(); logger.info("saveSMSTemplate received request"); diff --git a/src/main/java/com/iemr/common/exception/ValidationExceptionHandler.java b/src/main/java/com/iemr/common/exception/ValidationExceptionHandler.java new file mode 100644 index 00000000..750d221c --- /dev/null +++ b/src/main/java/com/iemr/common/exception/ValidationExceptionHandler.java @@ -0,0 +1,54 @@ +package com.iemr.common.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class ValidationExceptionHandler { + + private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + logger.error("Validation failed: {}", errors); + + Map response = new HashMap<>(); + response.put("status", "ERROR"); + response.put("statusCode", 5000); + response.put("errorMessage", "Input validation failed"); + response.put("errors", errors); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) { + logger.error("Illegal argument: {}", ex.getMessage()); + + Map response = new HashMap<>(); + response.put("status", "ERROR"); + response.put("statusCode", 5000); + response.put("errorMessage", ex.getMessage()); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } +} + + diff --git a/src/main/java/com/iemr/common/model/sms/CreateSMSRequest.java b/src/main/java/com/iemr/common/model/sms/CreateSMSRequest.java index 358730c9..774a8517 100644 --- a/src/main/java/com/iemr/common/model/sms/CreateSMSRequest.java +++ b/src/main/java/com/iemr/common/model/sms/CreateSMSRequest.java @@ -23,17 +23,32 @@ import java.util.List; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; import lombok.Data; @Data public class CreateSMSRequest { Integer smsTemplateID; + @NotBlank(message = "SMS template name is required") + @Size(min = 3, max = 100, message = "Template name must be between 3 and 100 characters") + @Pattern(regexp = "^[a-zA-Z0-9_\\s-]+$", + message = "Template name can only contain alphanumeric characters, spaces, hyphens and underscores") String smsTemplateName; + @NotBlank(message = "SMS template content is required") + @Size(min = 10, max = 500, message = "Template content must be between 10 and 500 characters") + @Pattern(regexp = "^[^<>]*$", + message = "Template cannot contain < or > characters") String smsTemplate; Integer smsTypeID; SMSTypeModel smsType; + @NotNull(message = "Provider service map ID is required") + @Positive(message = "Provider service map ID must be positive") Integer providerServiceMapID; String createdBy; + @Valid + @NotEmpty(message = "At least one SMS parameter is required") + @Size(max = 20, message = "Maximum 20 parameters allowed") List smsParameterMaps; } diff --git a/src/main/java/com/iemr/common/model/sms/SMSParameterMapModel.java b/src/main/java/com/iemr/common/model/sms/SMSParameterMapModel.java index 032e90d2..fb9133e7 100644 --- a/src/main/java/com/iemr/common/model/sms/SMSParameterMapModel.java +++ b/src/main/java/com/iemr/common/model/sms/SMSParameterMapModel.java @@ -21,6 +21,7 @@ */ package com.iemr.common.model.sms; +import jakarta.validation.constraints.*; import lombok.Data; @Data @@ -30,7 +31,14 @@ public class SMSParameterMapModel Integer smsTemplateID; String createdBy; String modifiedBy; + @Size(max = 200, message = "Parameter value must not exceed 200 characters") + @Pattern(regexp = "^[^<>\"';&|`$(){}\\[\\]]*$", + message = "Parameter value contains invalid characters") String smsParameterValue; + @NotBlank(message = "Parameter name is required") + @Size(min = 2, max = 50, message = "Parameter name must be between 2 and 50 characters") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]*$", + message = "Parameter name must start with a letter and contain only alphanumeric and underscore") String smsParameterName; String smsParameterType; } diff --git a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java index 022fd1f8..af9cbf1c 100644 --- a/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java +++ b/src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java @@ -104,10 +104,12 @@ import com.iemr.common.repository.videocall.VideoCallParameterRepository; import com.iemr.common.service.beneficiary.IEMRSearchUserService; import com.iemr.common.utils.CryptoUtil; +import com.iemr.common.utils.InputSanitizer; import com.iemr.common.utils.config.ConfigProperties; import com.iemr.common.utils.http.HttpUtils; import com.iemr.common.utils.mapper.OutputMapper; //import java.util.Date; +import org.springframework.transaction.annotation.Transactional; @Service public class SMSServiceImpl implements SMSService { @@ -206,7 +208,14 @@ public String updateSMSTemplate(UpdateSMSRequest smsRequest) throws Exception { } @Override + @Transactional(rollbackFor = Exception.class) public String saveSMSTemplate(CreateSMSRequest smsRequest) throws Exception { + // Sanitize inputs before processing + sanitizeInputs(smsRequest); + // Validate template syntax + if (!InputSanitizer.isValidTemplateParameter(smsRequest.getSmsTemplate())) { + throw new IllegalArgumentException("Template contains invalid parameter syntax"); + } SMSTemplate smsTemplate; SMSTemplate request = smsMapper.createRequestToSMSTemplate(smsRequest); smsTemplate = smsTemplateRepository.save(request); @@ -214,6 +223,45 @@ public String saveSMSTemplate(CreateSMSRequest smsRequest) throws Exception { smsTemplate = smsTemplateRepository.findBySmsTemplateID(smsTemplate.getSmsTemplateID()); return OutputMapper.gsonWithoutExposeRestriction().toJson(smsMapper.smsTemplateToResponse(smsTemplate)); } + + /** + * Sanitize all inputs to prevent XSS, SQL injection, and command injection + */ + private void sanitizeInputs(CreateSMSRequest smsRequest) { + logger.debug("Sanitizing SMS template request inputs"); + + // Sanitize template name + if (smsRequest.getSmsTemplateName() != null) { + smsRequest.setSmsTemplateName( + InputSanitizer.sanitize(smsRequest.getSmsTemplateName()) + ); + } + + // Sanitize template content (preserve ${} but remove dangerous chars) + if (smsRequest.getSmsTemplate() != null) { + smsRequest.setSmsTemplate( + InputSanitizer.sanitizeForXSS(smsRequest.getSmsTemplate()) + ); + } + + // Sanitize parameter maps + if (smsRequest.getSmsParameterMaps() != null) { + for (SMSParameterMapModel param : smsRequest.getSmsParameterMaps()) { + if (param.getSmsParameterName() != null) { + param.setSmsParameterName( + InputSanitizer.sanitize(param.getSmsParameterName()) + ); + } + if (param.getSmsParameterValue() != null) { + param.setSmsParameterValue( + InputSanitizer.sanitize(param.getSmsParameterValue()) + ); + } + } + } + + logger.debug("Input sanitization completed"); + } private void saveSMSParameterMaps(CreateSMSRequest smsRequest, Integer smsTemplateID) { List smsParameterMapModels = smsRequest.getSmsParameterMaps(); diff --git a/src/main/java/com/iemr/common/utils/InputSanitizer.java b/src/main/java/com/iemr/common/utils/InputSanitizer.java new file mode 100644 index 00000000..8c67f4fb --- /dev/null +++ b/src/main/java/com/iemr/common/utils/InputSanitizer.java @@ -0,0 +1,110 @@ +package com.iemr.common.utils; + +import org.springframework.web.util.HtmlUtils; + +/** + * Utility class for sanitizing user inputs to prevent XSS, SQL Injection, and Command Injection + */ +public class InputSanitizer { + + // Characters that are dangerous for XSS + private static final String[] XSS_PATTERNS = { + "", "javascript:", "onerror=", "onload=", + "", "<", "\\", "\n", "\r" + }; + + /** + * Sanitize input to prevent XSS attacks + * @param input User input string + * @return Sanitized string with HTML entities encoded + */ + public static String sanitizeForXSS(String input) { + if (input == null || input.trim().isEmpty()) { + return input; + } + + // HTML encode to neutralize XSS + String sanitized = HtmlUtils.htmlEscape(input); + + // Additional check for common XSS patterns (case-insensitive) + String lowerInput = sanitized.toLowerCase(); + for (String pattern : XSS_PATTERNS) { + if (lowerInput.contains(pattern.toLowerCase())) { + // Remove the dangerous pattern + sanitized = sanitized.replaceAll("(?i)" + pattern, ""); + } + } + + return sanitized; + } + + /** + * Sanitize input to prevent command injection + * Removes shell metacharacters + * @param input User input string + * @return Sanitized string + */ + public static String sanitizeForCommandInjection(String input) { + if (input == null || input.trim().isEmpty()) { + return input; + } + + String sanitized = input; + + // Remove dangerous command injection characters + for (String dangerChar : COMMAND_INJECTION_CHARS) { + sanitized = sanitized.replace(dangerChar, ""); + } + + return sanitized; + } + + /** + * Comprehensive sanitization for general text input + * Combines XSS and command injection protection + * @param input User input string + * @return Sanitized string + */ + public static String sanitize(String input) { + if (input == null) { + return null; + } + + // First remove command injection chars, then sanitize XSS + String sanitized = sanitizeForCommandInjection(input); + sanitized = sanitizeForXSS(sanitized); + + return sanitized.trim(); + } + + /** + * Validate that template parameter syntax is safe + * Allows ${paramName} but prevents ${`command`} style injections + * @param template Template string + * @return true if template is safe + */ + public static boolean isValidTemplateParameter(String template) { + if (template == null || template.trim().isEmpty()) { + return false; + } + + // Check for command injection attempts in template parameters + // Valid: ${userName}, ${age} + // Invalid: ${`ls`}, ${$(whoami)}, ${;rm -rf} + + if (template.contains("${`") || template.contains("$(`") || + template.contains("${$(") || template.contains("${;")) { + return false; + } + + return true; + } +} + +