I am facing a security issue where Spring web framwork’s RestTemplate logs the request body when debug mode is enabled.
My application connects with keycloak to get the token where it needs some parameters for authentications, for example – user and password. So, we provide these parameters as request parameters to RestTemplate in web service call.
Now here is the situation that RestTemplate logs these body parameters in plain text including the password.
I have tried some solutions on internet of intercepting the request and masking the password before logging, but solution doesn’t work as RestTemplate takes the body parameters from HttpEntity.
Sample of logs –
Writing [{client_id=[client1], grant_type=[password], username=[user1], password=[testit]}]
Below is the interceptor code which i tried –
public class MaskedLoggingRestTemplate2 extends RestTemplate {
private Set<String> maskedParameters;
private HttpEntity<MultiValueMap<String, Object>> requestEntity;
public MaskedLoggingRestTemplate2(HttpEntity<MultiValueMap<String, Object>> requestEntity) {
// Initialize the set of parameters to mask
maskedParameters = new HashSet<>();
maskedParameters.add("password");
this.requestEntity = requestEntity;
// maskedParameters.add("api_key");
// Add more parameters as needed
}
@Override
protected <T> T doExecute(URI url, @Nullable String uriTemplate, HttpMethod method, RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor){
RequestCallback maskedRequestCallback = new MaskedRequestCallback(requestCallback);
return super.doExecute(url, uriTemplate, method, maskedRequestCallback, responseExtractor);
}
private class MaskedRequestCallback implements RequestCallback {
private final RequestCallback delegate;
MaskedRequestCallback(RequestCallback delegate) {
this.delegate = delegate;
}
@Override
public void doWithRequest(ClientHttpRequest request) throws IOException {
delegate.doWithRequest(new MaskedClientHttpRequestWrapper(request));
}
}
private class MaskedClientHttpRequestWrapper implements ClientHttpRequest {
private final ClientHttpRequest delegate;
private byte[] bodyContent;
MaskedClientHttpRequestWrapper(ClientHttpRequest delegate) throws IOException {
this.delegate = delegate;
this.bodyContent = getMaskedBodyContent();
}
@Override
public HttpMethod getMethod() {
return delegate.getMethod();
}
@Override
public URI getURI() {
return delegate.getURI();
}
@Override
public HttpHeaders getHeaders() {
return delegate.getHeaders();
}
@Override
public OutputStream getBody() throws IOException {
FastByteArrayOutputStream byteArrayOutputStream = new FastByteArrayOutputStream();
byteArrayOutputStream.write(bodyContent);
return byteArrayOutputStream;
//return delegate.getBody();
}
@Override
public ClientHttpResponse execute() throws IOException {
ClientHttpResponse response = delegate.execute();
return new MaskedClientHttpResponseWrapper(response);
}
private byte[] getMaskedBodyContent() throws IOException {
// FastByteArrayOutputStream byteArrayOutputStream = (FastByteArrayOutputStream)delegate.getBody();
// byte[] originalBody = byteArrayOutputStream.toByteArray();
// String originalBodyString = new String(originalBody, StandardCharsets.UTF_8);
MultiValueMap<String, Object> requestEntityBody = requestEntity.getBody();
//Map<String, String> maskedParameterMap = new LinkedHashMap<>();
assert requestEntityBody != null;
LinkedHashMap<String, Object> maskedParameterMap = requestEntityBody.entrySet().stream()
.collect(LinkedHashMap::new,
(m, entry) -> {
m.put(entry.getKey(), maskedParameters.contains(entry.getKey()) ? "******" : entry.getValue());
},
LinkedHashMap::putAll);
// Split the body into individual parameters
// String[] parameters = originalBodyString.split("&");
// Map<String, String> maskedParameterMap = new LinkedHashMap<>();
// Mask each parameter as needed
// for (String parameter : parameters) {
// String[] keyValue = parameter.split("=");
// if (keyValue.length == 2) {
// String key = keyValue[0];
// String value = keyValue[1];
// if (maskedParameters.contains(key)) {
// value = "********"; // Mask the parameter value
// }
// maskedParameterMap.put(key, value);
// }
// }
// Recreate the body content with masked parameters
StringBuilder maskedBodyBuilder = new StringBuilder();
for (Map.Entry<String, Object> entry : maskedParameterMap.entrySet()) {
maskedBodyBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
if (maskedBodyBuilder.length() > 0) {
maskedBodyBuilder.deleteCharAt(maskedBodyBuilder.length() - 1);
}
return maskedBodyBuilder.toString().getBytes(StandardCharsets.UTF_8);
}
}
private class MaskedClientHttpResponseWrapper implements ClientHttpResponse {
private final ClientHttpResponse delegate;
MaskedClientHttpResponseWrapper(ClientHttpResponse delegate) {
this.delegate = delegate;
}
@Override
public HttpStatusCode getStatusCode() throws IOException {
return delegate.getStatusCode();
}
@Override
public String getStatusText() throws IOException {
return delegate.getStatusText();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return delegate.getBody();
}
@Override
public HttpHeaders getHeaders() {
return delegate.getHeaders();
}
// Implement other delegating methods as needed
}
Just a side note – RestTemplate has a method “logBody()” which is private and we can’t override it and it is called by “doWithRequest()”.
My expectation is to mask sensitive data like password before logging by RestTemplate OR If we can stop logging body by it.