security: filter content of known requests with passwords (elastic/elasticsearch#4700)

This commit adds a mechanism for defining known sensitive values in rest bodies so that
these can be filtered when auditing the request body.

Original commit: elastic/x-pack-elasticsearch@d138a6bff7
This commit is contained in:
Jay Modi 2017-01-20 14:05:23 -05:00 committed by GitHub
parent d690c5f789
commit 9005e9fdb9
8 changed files with 286 additions and 10 deletions

View File

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.rest;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.rest.RestRequest;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Identifies an object that supplies a filter for the content of a {@link RestRequest}. This interface should be implemented by a
* {@link org.elasticsearch.rest.RestHandler} that expects there will be sensitive content in the body of the request such as a password
*/
public interface RestRequestFilter {
/**
* Wraps the RestRequest and returns a version that provides the filtered content
*/
default RestRequest getFilteredRequest(RestRequest restRequest) throws IOException {
Set<String> fields = getFilteredFields();
if (restRequest.hasContent() && fields.isEmpty() == false) {
return new RestRequest(restRequest.getXContentRegistry(), restRequest.params(), restRequest.path()) {
private BytesReference filteredBytes = null;
@Override
public Method method() {
return restRequest.method();
}
@Override
public String uri() {
return restRequest.uri();
}
@Override
public boolean hasContent() {
return true;
}
@Override
public BytesReference content() {
if (filteredBytes == null) {
BytesReference content = restRequest.content();
Tuple<XContentType, Map<String, Object>> result = XContentHelper.convertToMap(content, true);
Map<String, Object> transformedSource = XContentMapValues.filter(result.v2(), null,
fields.toArray(Strings.EMPTY_ARRAY));
try {
XContentBuilder xContentBuilder = XContentBuilder.builder(result.v1().xContent()).map(transformedSource);
filteredBytes = xContentBuilder.bytes();
} catch (IOException e) {
throw new ElasticsearchException("failed to parse request", e);
}
}
return filteredBytes;
}
@Override
public String header(String name) {
return restRequest.header(name);
}
@Override
public Iterable<Entry<String, String>> headers() {
return restRequest.headers();
}
};
} else {
return restRequest;
}
}
/**
* The list of fields that should be filtered. This can be a dot separated pattern to match sub objects and also supports wildcards
*/
Set<String> getFilteredFields();
}

View File

@ -25,6 +25,8 @@ import org.elasticsearch.xpack.security.authc.AuthenticationService;
import org.elasticsearch.xpack.security.transport.ServerTransportFilter;
import org.elasticsearch.xpack.ssl.SSLService;
import java.io.IOException;
import static org.elasticsearch.xpack.XPackSettings.HTTP_SSL_ENABLED;
public class SecurityRestFilter implements RestHandler {
@ -58,7 +60,7 @@ public class SecurityRestFilter implements RestHandler {
assert handler != null;
ServerTransportFilter.extactClientCertificates(logger, threadContext, handler.engine(), nettyHttpRequest.getChannel());
}
service.authenticate(request, ActionListener.wrap(
service.authenticate(maybeWrapRestRequest(request), ActionListener.wrap(
authentication -> {
RemoteHostHeader.process(request, threadContext);
restHandler.handleRequest(request, channel, client);
@ -75,4 +77,11 @@ public class SecurityRestFilter implements RestHandler {
restHandler.handleRequest(request, channel, client);
}
}
RestRequest maybeWrapRestRequest(RestRequest restRequest) throws IOException {
if (restHandler instanceof RestRequestFilter) {
return ((RestRequestFilter)restHandler).getFilteredRequest(restRequest);
}
return restRequest;
}
}

View File

@ -18,14 +18,17 @@ import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.SecurityContext;
import org.elasticsearch.xpack.security.action.user.ChangePasswordResponse;
import org.elasticsearch.xpack.security.client.SecurityClient;
import org.elasticsearch.xpack.security.rest.RestRequestFilter;
import org.elasticsearch.xpack.security.user.User;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;
public class RestChangePasswordAction extends BaseRestHandler {
public class RestChangePasswordAction extends BaseRestHandler implements RestRequestFilter {
private final SecurityContext securityContext;
@ -62,4 +65,11 @@ public class RestChangePasswordAction extends BaseRestHandler {
}
});
}
private static final Set<String> FILTERED_FIELDS = Collections.singleton("password");
@Override
public Set<String> getFilteredFields() {
return FILTERED_FIELDS;
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.rest.action.user;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
@ -18,8 +19,11 @@ import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.action.user.PutUserRequestBuilder;
import org.elasticsearch.xpack.security.action.user.PutUserResponse;
import org.elasticsearch.xpack.security.client.SecurityClient;
import org.elasticsearch.xpack.security.rest.RestRequestFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;
@ -27,7 +31,8 @@ import static org.elasticsearch.rest.RestRequest.Method.PUT;
/**
* Rest endpoint to add a User to the security index
*/
public class RestPutUserAction extends BaseRestHandler {
public class RestPutUserAction extends BaseRestHandler implements RestRequestFilter {
public RestPutUserAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(POST, "/_xpack/security/user/{username}", this);
@ -58,4 +63,11 @@ public class RestPutUserAction extends BaseRestHandler {
}
});
}
private static final Set<String> FILTERED_FIELDS = Collections.unmodifiableSet(Sets.newHashSet("password", "passwordHash"));
@Override
public Set<String> getFilteredFields() {
return FILTERED_FIELDS;
}
}

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.watcher.rest.action;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
@ -17,6 +18,7 @@ import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.rest.RestRequestFilter;
import org.elasticsearch.xpack.watcher.client.WatcherClient;
import org.elasticsearch.xpack.watcher.execution.ActionExecutionMode;
import org.elasticsearch.xpack.watcher.rest.WatcherRestHandler;
@ -26,13 +28,16 @@ import org.elasticsearch.xpack.watcher.transport.actions.execute.ExecuteWatchReq
import org.elasticsearch.xpack.watcher.transport.actions.execute.ExecuteWatchResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;
import static org.elasticsearch.xpack.watcher.rest.action.RestExecuteWatchAction.Field.IGNORE_CONDITION;
import static org.elasticsearch.xpack.watcher.rest.action.RestExecuteWatchAction.Field.RECORD_EXECUTION;
public class RestExecuteWatchAction extends WatcherRestHandler {
public class RestExecuteWatchAction extends WatcherRestHandler implements RestRequestFilter {
public RestExecuteWatchAction(Settings settings, RestController controller) {
super(settings);
@ -132,6 +137,17 @@ public class RestExecuteWatchAction extends WatcherRestHandler {
return builder.request();
}
private static final Set<String> FILTERED_FIELDS = Collections.unmodifiableSet(
Sets.newHashSet("watch.input.http.request.auth.basic.password",
"watch.input.chain.inputs.*.http.request.auth.basic.password",
"watch.actions.*.email.attachments.*.reporting.auth.basic.password",
"watch.actions.*.webhook.auth.basic.password"));
@Override
public Set<String> getFilteredFields() {
return FILTERED_FIELDS;
}
interface Field {
ParseField ID = new ParseField("_id");
ParseField WATCH_RECORD = new ParseField("watch_record");

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.watcher.rest.action;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
@ -13,19 +14,23 @@ import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.security.rest.RestRequestFilter;
import org.elasticsearch.xpack.watcher.client.WatcherClient;
import org.elasticsearch.xpack.watcher.rest.WatcherRestHandler;
import org.elasticsearch.xpack.watcher.transport.actions.put.PutWatchRequest;
import org.elasticsearch.xpack.watcher.transport.actions.put.PutWatchResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;
import static org.elasticsearch.rest.RestStatus.CREATED;
import static org.elasticsearch.rest.RestStatus.OK;
public class RestPutWatchAction extends WatcherRestHandler {
public class RestPutWatchAction extends WatcherRestHandler implements RestRequestFilter {
public RestPutWatchAction(Settings settings, RestController controller) {
super(settings);
@ -55,4 +60,12 @@ public class RestPutWatchAction extends WatcherRestHandler {
});
}
private static final Set<String> FILTERED_FIELDS = Collections.unmodifiableSet(
Sets.newHashSet("input.http.request.auth.basic.password", "input.chain.inputs.*.http.request.auth.basic.password",
"actions.*.email.attachments.*.reporting.auth.basic.password", "actions.*.webhook.auth.basic.password"));
@Override
public Set<String> getFilteredFields() {
return FILTERED_FIELDS;
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.rest;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.rest.FakeRestRequest;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
public class RestRequestFilterTests extends ESTestCase {
public void testFilteringItemsInSubLevels() throws IOException {
BytesReference content = new BytesArray("{\"root\": {\"second\": {\"third\": \"password\", \"foo\": \"bar\"}}}");
RestRequestFilter filter = () -> Collections.singleton("root.second.third");
FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent(content).build();
RestRequest filtered = filter.getFilteredRequest(restRequest);
assertNotEquals(content, filtered.content());
Map<String, Object> map = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, filtered.content()).map();
Map<String, Object> root = (Map<String, Object>) map.get("root");
assertNotNull(root);
Map<String, Object> second = (Map<String, Object>) root.get("second");
assertNotNull(second);
assertEquals("bar", second.get("foo"));
assertNull(second.get("third"));
}
public void testFilteringItemsInSubLevelsWithWildCard() throws IOException {
BytesReference content = new BytesArray("{\"root\": {\"second\": {\"third\": \"password\", \"foo\": \"bar\"}}}");
RestRequestFilter filter = () -> Collections.singleton("root.*.third");
FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent(content).build();
RestRequest filtered = filter.getFilteredRequest(restRequest);
assertNotEquals(content, filtered.content());
Map<String, Object> map = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, filtered.content()).map();
Map<String, Object> root = (Map<String, Object>) map.get("root");
assertNotNull(root);
Map<String, Object> second = (Map<String, Object>) root.get("second");
assertNotNull(second);
assertEquals("bar", second.get("foo"));
assertNull(second.get("third"));
}
public void testFilteringItemsInSubLevelsWithLeadingWildCard() throws IOException {
BytesReference content = new BytesArray("{\"root\": {\"second\": {\"third\": \"password\", \"foo\": \"bar\"}}}");
RestRequestFilter filter = () -> Collections.singleton("*.third");
FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withContent(content).build();
RestRequest filtered = filter.getFilteredRequest(restRequest);
assertNotEquals(content, filtered.content());
Map<String, Object> map = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, filtered.content()).map();
Map<String, Object> root = (Map<String, Object>) map.get("root");
assertNotNull(root);
Map<String, Object> second = (Map<String, Object>) root.get("second");
assertNotNull(second);
assertEquals("bar", second.get("foo"));
assertNull(second.get("third"));
}
}

View File

@ -5,10 +5,14 @@
*/
package org.elasticsearch.xpack.security.rest;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.rest.BytesRestResponse;
@ -17,13 +21,19 @@ import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.test.rest.FakeRestRequest;
import org.elasticsearch.xpack.security.authc.Authentication;
import org.elasticsearch.xpack.security.authc.Authentication.RealmRef;
import org.elasticsearch.xpack.security.authc.AuthenticationService;
import org.elasticsearch.xpack.security.user.XPackUser;
import org.elasticsearch.xpack.ssl.SSLService;
import org.junit.Before;
import org.mockito.ArgumentCaptor;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import static org.elasticsearch.xpack.security.support.Exceptions.authenticationError;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
@ -35,6 +45,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
public class SecurityRestFilterTests extends ESTestCase {
private AuthenticationService authcService;
private RestChannel channel;
private SecurityRestFilter filter;
@ -48,10 +59,8 @@ public class SecurityRestFilterTests extends ESTestCase {
licenseState = mock(XPackLicenseState.class);
when(licenseState.isAuthAllowed()).thenReturn(true);
restHandler = mock(RestHandler.class);
ThreadPool threadPool = mock(ThreadPool.class);
when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY));
filter = new SecurityRestFilter(Settings.EMPTY, licenseState, mock(SSLService.class),
threadPool.getThreadContext(), authcService, restHandler);
new ThreadContext(Settings.EMPTY), authcService, restHandler);
}
public void testProcess() throws Exception {
@ -102,4 +111,52 @@ public class SecurityRestFilterTests extends ESTestCase {
verifyZeroInteractions(channel);
verifyZeroInteractions(authcService);
}
public void testProcessFiltersBodyCorrectly() throws Exception {
FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY)
.withContent(new BytesArray("{\"password\": \"changeme\", \"foo\": \"bar\"}")).build();
when(channel.request()).thenReturn(restRequest);
SetOnce<RestRequest> handlerRequest = new SetOnce<>();
restHandler = new FilteredRestHandler() {
@Override
public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception {
handlerRequest.set(request);
}
@Override
public Set<String> getFilteredFields() {
return Collections.singleton("password");
}
};
SetOnce<RestRequest> authcServiceRequest = new SetOnce<>();
doAnswer((i) -> {
ActionListener callback =
(ActionListener) i.getArguments()[1];
authcServiceRequest.set((RestRequest)i.getArguments()[0]);
callback.onResponse(new Authentication(XPackUser.INSTANCE, new RealmRef("test", "test", "t"), null));
return Void.TYPE;
}).when(authcService).authenticate(any(RestRequest.class), any(ActionListener.class));
filter = new SecurityRestFilter(Settings.EMPTY, licenseState, mock(SSLService.class),
new ThreadContext(Settings.EMPTY), authcService, restHandler);
filter.handleRequest(restRequest, channel, null);
assertEquals(restRequest, handlerRequest.get());
assertEquals(restRequest.content(), handlerRequest.get().content());
Map<String, Object> original = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, handlerRequest.get()
.content()).map();
assertEquals(2, original.size());
assertEquals("changeme", original.get("password"));
assertEquals("bar", original.get("foo"));
assertNotEquals(restRequest, authcServiceRequest.get());
assertNotEquals(restRequest.content(), authcServiceRequest.get().content());
Map<String, Object> map = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, authcServiceRequest.get()
.content()).map();
assertEquals(1, map.size());
assertEquals("bar", map.get("foo"));
}
private interface FilteredRestHandler extends RestHandler, RestRequestFilter {}
}