Let's mask properly when outputting Spring Binding Result (Errors) to log

In Web application using Spring (Spring MVC), input check error information is expressed by BindingResult interface and can be received as an argument of Controller's handler method as shown below.

@PostMapping
public String change(@AuthenticationPrincipal AccountUserDetails userDetails,
    @Validated PasswordChangeForm form, BindingResult result) { // 
  if (result.hasErrors()) {
    return changeForm();
  }
  accountService.changePassword(userDetails.getAccount(), form.getNewPassword());
  return "welcome/home";
}

There is no problem with the above code, but let's add the following code and output the error information to the console.

System.out.println(result);

The following information is output to the console.

org.springframework.validation.BeanPropertyBindingResult: 6 errors
Field error in object 'passwordChangeForm' on field 'confirmPassword': rejected value [ccc]; codes [com.example.validation.Confirm.message.passwordChangeForm.confirmPassword,com.example.validation.Confirm.message.confirmPassword,com.example.validation.Confirm.message.java.lang.String,com.example.validation.Confirm.message]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.confirmPassword,confirmPassword]; arguments []; default message [confirmPassword],org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@41663a9a,PROPERTY,EQUAL,false,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@7dd16c6]; default message [must same value with "newPassword"]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [bb]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must contain 1 or more special characters.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [bb]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must be 8 or more characters in length.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [bb]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must contain 1 or more uppercase characters.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [bb]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must contain 1 or more digit characters.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [bb]; codes [error.passwordChangeForm.newPassword,error.newPassword,error.java.lang.String,error]; arguments []; default message [error!]

The error "field", "actual input value", "error code", "message argument", "default message", etc. are output, but ** "actual input value (rejected value [ccc];" part Please note that ")" is output **. In the above example, ** the value of "password" that should be treated as confidential information is output as it is. ** ** ** In other words ... If there is a requirement to log an input check error, simply using BindingResult # toString () will result in an application that does not meet the security requirements. ** **

Note:

Similar information may be recorded in logs etc. without implementing the code that explicitly calls BindingResult # toString () as in the above example. For example ... If you don't specify BindingResult in the argument of Controller's handler method (= omitted), Spring will generateBindException or MethodArgumentNotValidException. Since these exception messages contain the same content as BindingResult # toString (), if the implementation is such that the log is output unconditionally with an exception handler etc., confidential information will be unintentionally displayed. It will be output to the log.

What should I do?

Easy to answer! Do not use "BindingResult # toString ()" for logs output in a commercial environment! !! about it. The input check error is output to the log in the web application for UI! It seems that there are not many specific requirements, but ... I think that there are cases where it is required to output logs for error analysis in Web applications (Web API, REST API) for inter-system cooperation. In such a case, instead of simply using "BindingResult # toString ()", let's create an appropriate log message from the error information of BindingResult. Also, make sure to handle BindException and MethodArgumentNotValidException separately in the exception handler implementation.

Try masking confidential information

here,

Let's assemble a log message with the requirement.

final StringJoiner joiner = new StringJoiner("\n")
    .add(result.getClass().getName() + ":" + result.getErrorCount() + " errors");
result.getGlobalErrors().forEach(error -> joiner.add(error.toString()));
result.getFieldErrors().forEach(error -> {
  if (error.getField().toLowerCase().contains("password")) {
    String message = error.toString();
    int sIndex = message.indexOf("rejected value [") + 16;
    int eIndex = message.indexOf("]; codes");
    joiner.add(message.substring(0, sIndex) + IntStream.range(0, eIndex - sIndex)
        .mapToObj(value -> "*").collect(Collectors.joining()) + message.substring(eIndex));
  } else {
    joiner.add(error.toString());
  }
});
System.out.println(joiner.toString());

I feel that it is a little forcible dribbling (implementation), but the message assembled with the above logic is as follows.

org.springframework.validation.BeanPropertyBindingResult:6 errors
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [**]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must contain 1 or more uppercase characters.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [**]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must be 8 or more characters in length.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [**]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must contain 1 or more digit characters.]
Field error in object 'passwordChangeForm' on field 'confirmPassword': rejected value [***]; codes [com.example.validation.Confirm.message.passwordChangeForm.confirmPassword,com.example.validation.Confirm.message.confirmPassword,com.example.validation.Confirm.message.java.lang.String,com.example.validation.Confirm.message]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.confirmPassword,confirmPassword]; arguments []; default message [confirmPassword],org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@6d154140,PROPERTY,EQUAL,false,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@41678dbe]; default message [must same value with "newPassword"]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [**]; codes [Password.passwordChangeForm.newPassword,Password.newPassword,Password.java.lang.String,Password]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordChangeForm.newPassword,newPassword]; arguments []; default message [newPassword],true]; default message [must contain 1 or more special characters.]
Field error in object 'passwordChangeForm' on field 'newPassword': rejected value [**]; codes [error.passwordChangeForm.newPassword,error.newPassword,error.java.lang.String,error]; arguments []; default message [error!]

Summary

Depending on the system, it may be required to mask confidential information when outputting a message (telegram?) To the log, but the application log is also the same. It was a story. In this post, I introduced an implementation example using a technique called masking, but masking is not the only way to protect confidential information, so let's do it in a way that suits the characteristics and requirements of the system! !!

Recommended Posts

Let's mask properly when outputting Spring Binding Result (Errors) to log
To avoid errors when starting miChecker