Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, Object>> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> 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<String, Object> 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<Map<String, Object>> handleIllegalArgument(IllegalArgumentException ex) {
logger.error("Illegal argument: {}", ex.getMessage());

Map<String, Object> response = new HashMap<>();
response.put("status", "ERROR");
response.put("statusCode", 5000);
response.put("errorMessage", ex.getMessage());

return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}


15 changes: 15 additions & 0 deletions src/main/java/com/iemr/common/model/sms/CreateSMSRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<SMSParameterMapModel> smsParameterMaps;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
package com.iemr.common.model.sms;

import jakarta.validation.constraints.*;
import lombok.Data;

@Data
Expand All @@ -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;
}
48 changes: 48 additions & 0 deletions src/main/java/com/iemr/common/service/sms/SMSServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -206,14 +208,60 @@ 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);
saveSMSParameterMaps(smsRequest, smsTemplate.getSmsTemplateID());
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<SMSParameterMapModel> smsParameterMapModels = smsRequest.getSmsParameterMaps();
Expand Down
110 changes: 110 additions & 0 deletions src/main/java/com/iemr/common/utils/InputSanitizer.java
Original file line number Diff line number Diff line change
@@ -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 = {
"<script", "</script>", "javascript:", "onerror=", "onload=",
"<iframe", "<object", "<embed", "eval(", "expression("
};

// Characters that could be used for command injection
private static final String[] COMMAND_INJECTION_CHARS = {
";", "|", "&", "`", "$", "(", ")", "{", "}", "[", "]",
"&&", "||", ">", "<", "\\", "\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;
}
}


Loading