Skip to content

Commit 480561d

Browse files
Store and use only internal security headers (#66365)
For async searches (EQL included) the client's request headers were erroneously stored in the .tasks index. This might expose the requesting client's HTTP Authorization header. This PR fixes that by employing the usual approach to store only the security-internal headers, which carry the authentication result, instead of the original Authorization header, which is commonly utilized to redo authentication for scheduled tasks.
1 parent 50aca62 commit 480561d

File tree

24 files changed

+84
-65
lines changed

24 files changed

+84
-65
lines changed

server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,13 @@ public String getHeader(String key) {
324324
}
325325

326326
/**
327-
* Returns all of the request contexts headers
327+
* Returns all of the request headers from the thread's context.<br>
328+
* <b>Be advised, headers might contain credentials.</b>
329+
* In order to avoid storing, and erroneously exposing, such headers,
330+
* it is recommended to instead store security headers that prove
331+
* the credentials have been verified successfully, and which are
332+
* internal to the system, in the sense that they cannot be sent
333+
* by the clients.
328334
*/
329335
public Map<String, String> getHeaders() {
330336
HashMap<String, String> map = new HashMap<>(defaultHeader);

x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/TransportSubmitAsyncSearchAction.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.tasks.Task;
3333
import org.elasticsearch.tasks.TaskId;
3434
import org.elasticsearch.transport.TransportService;
35+
import org.elasticsearch.xpack.core.ClientHelper;
3536
import org.elasticsearch.xpack.core.XPackPlugin;
3637
import org.elasticsearch.xpack.core.async.AsyncExecutionId;
3738
import org.elasticsearch.xpack.core.async.AsyncTaskIndexService;
@@ -136,7 +137,7 @@ public void onFailure(Exception exc) {
136137

137138
private SearchRequest createSearchRequest(SubmitAsyncSearchRequest request, Task submitTask, TimeValue keepAlive) {
138139
String docID = UUIDs.randomBase64UUID();
139-
Map<String, String> originHeaders = nodeClient.threadPool().getThreadContext().getHeaders();
140+
Map<String, String> originHeaders = ClientHelper.filterSecurityHeaders(nodeClient.threadPool().getThreadContext().getHeaders());
140141
SearchRequest searchRequest = new SearchRequest(request.getSearchRequest()) {
141142
@Override
142143
public AsyncSearchTask createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> taskHeaders) {

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import org.elasticsearch.license.XPackLicenseState;
3838
import org.elasticsearch.rest.RestStatus;
3939
import org.elasticsearch.xpack.ccr.action.ShardChangesAction;
40-
import org.elasticsearch.xpack.ccr.action.ShardFollowTask;
40+
import org.elasticsearch.xpack.core.ClientHelper;
4141
import org.elasticsearch.xpack.core.XPackPlugin;
4242
import org.elasticsearch.xpack.core.security.SecurityContext;
4343
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
@@ -58,7 +58,6 @@
5858
import java.util.function.Consumer;
5959
import java.util.function.Function;
6060
import java.util.function.Supplier;
61-
import java.util.stream.Collectors;
6261

6362
/**
6463
* Encapsulates licensing checking for CCR.
@@ -363,18 +362,15 @@ public static Client wrapClient(Client client, Map<String, String> headers) {
363362
if (headers.isEmpty()) {
364363
return client;
365364
} else {
366-
final ThreadContext threadContext = client.threadPool().getThreadContext();
367-
Map<String, String> filteredHeaders = headers.entrySet().stream()
368-
.filter(e -> ShardFollowTask.HEADER_FILTERS.contains(e.getKey()))
369-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
365+
Map<String, String> filteredHeaders = ClientHelper.filterSecurityHeaders(headers);
366+
if (filteredHeaders.isEmpty()) {
367+
return client;
368+
}
370369
return new FilterClient(client) {
371370
@Override
372371
protected <Request extends ActionRequest, Response extends ActionResponse>
373372
void doExecute(ActionType<Response> action, Request request, ActionListener<Response> listener) {
374-
final Supplier<ThreadContext.StoredContext> supplier = threadContext.newRestorableContext(false);
375-
try (ThreadContext.StoredContext ignore = stashWithHeaders(threadContext, filteredHeaders)) {
376-
super.doExecute(action, request, new ContextPreservingActionListener<>(supplier, listener));
377-
}
373+
ClientHelper.executeWithHeadersAsync(filteredHeaders, null, client, action, request, listener);
378374
}
379375
};
380376
}

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTask.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,14 @@
2121
import org.elasticsearch.xpack.core.ccr.action.ImmutableFollowParameters;
2222

2323
import java.io.IOException;
24-
import java.util.Arrays;
2524
import java.util.Collections;
26-
import java.util.HashSet;
2725
import java.util.Map;
2826
import java.util.Objects;
29-
import java.util.Set;
3027

3128
public class ShardFollowTask extends ImmutableFollowParameters implements XPackPlugin.XPackPersistentTaskParams {
3229

3330
public static final String NAME = "xpack/ccr/shard_follow_task";
3431

35-
// list of headers that will be stored when a job is created
36-
public static final Set<String> HEADER_FILTERS =
37-
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("es-security-runas-user", "_xpack_security_authentication")));
38-
3932
private static final ParseField REMOTE_CLUSTER_FIELD = new ParseField("remote_cluster");
4033
private static final ParseField FOLLOW_SHARD_INDEX_FIELD = new ParseField("follow_shard_index");
4134
private static final ParseField FOLLOW_SHARD_INDEX_UUID_FIELD = new ParseField("follow_shard_index_uuid");

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPutAutoFollowPatternAction.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.threadpool.ThreadPool;
2828
import org.elasticsearch.transport.TransportService;
2929
import org.elasticsearch.xpack.ccr.CcrLicenseChecker;
30+
import org.elasticsearch.xpack.core.ClientHelper;
3031
import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata;
3132
import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata.AutoFollowPattern;
3233
import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction;
@@ -92,9 +93,7 @@ protected void masterOperation(PutAutoFollowPatternAction.Request request,
9293
return;
9394
}
9495
final Client remoteClient = client.getRemoteClusterClient(request.getRemoteCluster());
95-
final Map<String, String> filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream()
96-
.filter(e -> ShardFollowTask.HEADER_FILTERS.contains(e.getKey()))
97-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
96+
final Map<String, String> filteredHeaders = ClientHelper.filterSecurityHeaders(threadPool.getThreadContext().getHeaders());
9897

9998
Consumer<ClusterStateResponse> consumer = remoteClusterState -> {
10099
String[] indices = request.getLeaderIndexPatterns().toArray(new String[0]);

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportResumeFollowAction.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.elasticsearch.xpack.ccr.Ccr;
4848
import org.elasticsearch.xpack.ccr.CcrLicenseChecker;
4949
import org.elasticsearch.xpack.ccr.CcrSettings;
50+
import org.elasticsearch.xpack.core.ClientHelper;
5051
import org.elasticsearch.xpack.core.ccr.action.FollowParameters;
5152
import org.elasticsearch.xpack.core.ccr.action.ResumeFollowAction;
5253

@@ -58,7 +59,6 @@
5859
import java.util.Map;
5960
import java.util.Objects;
6061
import java.util.Set;
61-
import java.util.stream.Collectors;
6262

6363
public class TransportResumeFollowAction extends TransportMasterNodeAction<ResumeFollowAction.Request, AcknowledgedResponse> {
6464

@@ -173,9 +173,7 @@ void start(
173173
validate(request, leaderIndexMetadata, followIndexMetadata, leaderIndexHistoryUUIDs, mapperService);
174174
final int numShards = followIndexMetadata.getNumberOfShards();
175175
final ResponseHandler handler = new ResponseHandler(numShards, listener);
176-
Map<String, String> filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream()
177-
.filter(e -> ShardFollowTask.HEADER_FILTERS.contains(e.getKey()))
178-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
176+
Map<String, String> filteredHeaders = ClientHelper.filterSecurityHeaders(threadPool.getThreadContext().getHeaders());
179177

180178
for (int shardId = 0; shardId < numShards; shardId++) {
181179
String taskId = followIndexMetadata.getIndexUUID() + "-" + shardId;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
import org.elasticsearch.persistent.PersistentTasksService;
1919
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
2020
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
21+
import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication;
2122

2223
import java.util.Map;
2324
import java.util.Objects;
2425
import java.util.Set;
2526
import java.util.function.BiConsumer;
2627
import java.util.function.Supplier;
28+
import java.util.regex.Pattern;
2729
import java.util.stream.Collectors;
2830

2931
/**
@@ -32,13 +34,27 @@
3234
*/
3335
public final class ClientHelper {
3436

37+
private static Pattern authorizationHeaderPattern = Pattern.compile("\\s*" + Pattern.quote("Authorization") + "\\s*",
38+
Pattern.CASE_INSENSITIVE);
39+
40+
public static void assertNoAuthorizationHeader(Map<String, String> headers) {
41+
if (org.elasticsearch.Assertions.ENABLED) {
42+
for (String header : headers.keySet()) {
43+
if (authorizationHeaderPattern.matcher(header).find()) {
44+
assert false : "headers contain \"Authorization\"";
45+
}
46+
}
47+
}
48+
}
49+
3550
/**
3651
* List of headers that are related to security
3752
*/
3853
public static final Set<String> SECURITY_HEADER_FILTERS =
3954
Sets.newHashSet(
4055
AuthenticationServiceField.RUN_AS_USER_HEADER,
41-
AuthenticationField.AUTHENTICATION_KEY);
56+
AuthenticationField.AUTHENTICATION_KEY,
57+
SecondaryAuthentication.THREAD_CTX_KEY);
4258

4359
/**
4460
* Leaves only headers that are related to security and filters out the rest.
@@ -47,9 +63,14 @@ public final class ClientHelper {
4763
* @return A portion of entries that are related to security
4864
*/
4965
public static Map<String, String> filterSecurityHeaders(Map<String, String> headers) {
50-
return Objects.requireNonNull(headers).entrySet().stream()
51-
.filter(e -> SECURITY_HEADER_FILTERS.contains(e.getKey()))
52-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
66+
if (SECURITY_HEADER_FILTERS.containsAll(headers.keySet())) {
67+
// fast-track to skip the artifice below
68+
return headers;
69+
} else {
70+
return Objects.requireNonNull(headers).entrySet().stream()
71+
.filter(e -> SECURITY_HEADER_FILTERS.contains(e.getKey()))
72+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
73+
}
5374
}
5475

5576
/**
@@ -162,11 +183,8 @@ public static <T extends ActionResponse> T executeWithHeaders(Map<String, String
162183
public static <Request extends ActionRequest, Response extends ActionResponse>
163184
void executeWithHeadersAsync(Map<String, String> headers, String origin, Client client, ActionType<Response> action, Request request,
164185
ActionListener<Response> listener) {
165-
166-
Map<String, String> filteredHeaders = filterSecurityHeaders(headers);
167-
186+
final Map<String, String> filteredHeaders = filterSecurityHeaders(headers);
168187
final ThreadContext threadContext = client.threadPool().getThreadContext();
169-
170188
// No headers (e.g. security not installed/in use) so execute as origin
171189
if (filteredHeaders.isEmpty()) {
172190
ClientHelper.executeAsyncWithOrigin(client, origin, action, request, listener);
@@ -181,6 +199,7 @@ void executeWithHeadersAsync(Map<String, String> headers, String origin, Client
181199

182200
private static ThreadContext.StoredContext stashWithHeaders(ThreadContext threadContext, Map<String, String> headers) {
183201
final ThreadContext.StoredContext storedContext = threadContext.stashContext();
202+
assertNoAuthorizationHeader(headers);
184203
threadContext.copyHeaders(headers.entrySet());
185204
return storedContext;
186205
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import java.util.Random;
5151
import java.util.concurrent.TimeUnit;
5252

53+
import static org.elasticsearch.xpack.core.ClientHelper.assertNoAuthorizationHeader;
54+
5355
/**
5456
* Datafeed configuration options. Describes where to proactively pull input
5557
* data from.
@@ -506,6 +508,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
506508
builder.field(CHUNKING_CONFIG.getPreferredName(), chunkingConfig);
507509
}
508510
if (headers.isEmpty() == false && params.paramAsBoolean(ToXContentParams.FOR_INTERNAL_STORAGE, false)) {
511+
assertNoAuthorizationHeader(headers);
509512
builder.field(HEADERS.getPreferredName(), headers);
510513
}
511514
if (delayedDataCheckConfig != null) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/DataFrameAnalyticsConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import static org.elasticsearch.common.xcontent.ObjectParser.ValueType.OBJECT_ARRAY_BOOLEAN_OR_STRING;
3737
import static org.elasticsearch.common.xcontent.ObjectParser.ValueType.VALUE;
38+
import static org.elasticsearch.xpack.core.ClientHelper.assertNoAuthorizationHeader;
3839

3940
public class DataFrameAnalyticsConfig implements ToXContentObject, Writeable {
4041

@@ -251,6 +252,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
251252
}
252253
builder.field(MODEL_MEMORY_LIMIT.getPreferredName(), getModelMemoryLimit().getStringRep());
253254
if (headers.isEmpty() == false && params.paramAsBoolean(ToXContentParams.FOR_INTERNAL_STORAGE, false)) {
255+
assertNoAuthorizationHeader(headers);
254256
builder.field(HEADERS.getPreferredName(), headers);
255257
}
256258
if (createTime != null) {
@@ -414,6 +416,7 @@ public Builder setAnalyzedFields(FetchSourceContext fields) {
414416

415417
public Builder setHeaders(Map<String, String> headers) {
416418
this.headers = headers;
419+
assertNoAuthorizationHeader(this.headers);
417420
return this;
418421
}
419422

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/job/RollupJob.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Map;
2222
import java.util.Objects;
2323

24+
import static org.elasticsearch.xpack.core.ClientHelper.assertNoAuthorizationHeader;
25+
2426
/**
2527
* This class is the main wrapper object that is serialized into the PersistentTask's cluster state.
2628
* It holds the config (RollupJobConfig) and a map of authentication headers. Only RollupJobConfig
@@ -67,6 +69,7 @@ public Map<String, String> getHeaders() {
6769
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
6870
builder.startObject();
6971
builder.field(CONFIG.getPreferredName(), config);
72+
assertNoAuthorizationHeader(headers);
7073
builder.field(HEADERS.getPreferredName(), headers);
7174
builder.endObject();
7275
return builder;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/SecondaryAuthentication.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626
public class SecondaryAuthentication {
2727

28-
private static final String THREAD_CTX_KEY = "_xpack_security_secondary_authc";
28+
public static final String THREAD_CTX_KEY = "_xpack_security_secondary_authc";
2929

3030
private final SecurityContext securityContext;
3131
private final Authentication authentication;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SnapshotLifecyclePolicyMetadata.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.Objects;
2525
import java.util.Optional;
2626

27+
import static org.elasticsearch.xpack.core.ClientHelper.assertNoAuthorizationHeader;
28+
2729
/**
2830
* {@code SnapshotLifecyclePolicyMetadata} encapsulates a {@link SnapshotLifecyclePolicy} as well as
2931
* the additional meta information link headers used for execution, version (a monotonically
@@ -86,6 +88,7 @@ public static SnapshotLifecyclePolicyMetadata parse(XContentParser parser, Strin
8688
SnapshotInvocationRecord lastSuccess, SnapshotInvocationRecord lastFailure) {
8789
this.policy = policy;
8890
this.headers = headers;
91+
assertNoAuthorizationHeader(this.headers);
8992
this.version = version;
9093
this.modifiedDate = modifiedDate;
9194
this.lastSuccess = lastSuccess;
@@ -96,6 +99,7 @@ public static SnapshotLifecyclePolicyMetadata parse(XContentParser parser, Strin
9699
SnapshotLifecyclePolicyMetadata(StreamInput in) throws IOException {
97100
this.policy = new SnapshotLifecyclePolicy(in);
98101
this.headers = (Map<String, String>) in.readGenericValue();
102+
assertNoAuthorizationHeader(this.headers);
99103
this.version = in.readVLong();
100104
this.modifiedDate = in.readVLong();
101105
this.lastSuccess = in.readOptionalWriteable(SnapshotInvocationRecord::new);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/execution/WatchExecutionContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.concurrent.ConcurrentMap;
2929
import java.util.concurrent.TimeUnit;
3030

31+
import static org.elasticsearch.xpack.core.ClientHelper.assertNoAuthorizationHeader;
32+
3133
public abstract class WatchExecutionContext {
3234

3335
private final Wid id;
@@ -261,6 +263,7 @@ public WatchExecutionSnapshot createSnapshot(Thread executionThread) {
261263
*/
262264
public static String getUsernameFromWatch(Watch watch) throws IOException {
263265
if (watch != null && watch.status() != null && watch.status().getHeaders() != null) {
266+
assertNoAuthorizationHeader(watch.status().getHeaders());
264267
String header = watch.status().getHeaders().get(AuthenticationField.AUTHENTICATION_KEY);
265268
if (header != null) {
266269
Authentication auth = AuthenticationContextSerializer.decode(header);

x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/async/AsyncTaskManagementService.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.tasks.TaskManager;
2929
import org.elasticsearch.threadpool.Scheduler;
3030
import org.elasticsearch.threadpool.ThreadPool;
31+
import org.elasticsearch.xpack.core.ClientHelper;
3132
import org.elasticsearch.xpack.core.async.AsyncExecutionId;
3233
import org.elasticsearch.xpack.core.async.AsyncTask;
3334
import org.elasticsearch.xpack.core.async.AsyncTaskIndexService;
@@ -91,8 +92,9 @@ public TaskId getParentTask() {
9192

9293
@Override
9394
public Task createTask(long id, String type, String action, TaskId parentTaskId, Map<String, String> headers) {
94-
return operation.createTask(request, id, type, action, parentTaskId, headers, threadPool.getThreadContext().getHeaders(),
95-
new AsyncExecutionId(doc, new TaskId(node, id)));
95+
Map<String, String> originHeaders = ClientHelper.filterSecurityHeaders(threadPool.getThreadContext().getHeaders());
96+
return operation.createTask(request, id, type, action, parentTaskId, headers, originHeaders, new AsyncExecutionId(doc,
97+
new TaskId(node, id)));
9698
}
9799

98100
@Override
@@ -193,7 +195,7 @@ private void storeResults(T searchTask, StoredAsyncResponse<Response> storedResp
193195
private void storeResults(T searchTask, StoredAsyncResponse<Response> storedResponse, ActionListener<Void> finalListener) {
194196
try {
195197
asyncTaskIndexService.createResponse(searchTask.getExecutionId().getDocId(),
196-
threadPool.getThreadContext().getHeaders(), storedResponse, ActionListener.wrap(
198+
searchTask.getOriginHeaders(), storedResponse, ActionListener.wrap(
197199
// We should only unregister after the result is saved
198200
resp -> {
199201
logger.trace(() -> new ParameterizedMessage("stored eql search results for [{}]",

x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ protected void masterOperation(Request request, ClusterState state, ActionListen
9393
// REST layer and the Transport layer here must be accessed within this thread and not in the
9494
// cluster state thread in the ClusterStateUpdateTask below since that thread does not share the
9595
// same context, and therefore does not have access to the appropriate security headers.
96-
Map<String, String> filteredHeaders = threadPool.getThreadContext().getHeaders().entrySet().stream()
97-
.filter(e -> ClientHelper.SECURITY_HEADER_FILTERS.contains(e.getKey()))
98-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
96+
Map<String, String> filteredHeaders = ClientHelper.filterSecurityHeaders(threadPool.getThreadContext().getHeaders());
9997
LifecyclePolicy.validatePolicyName(request.getPolicy().getName());
10098
clusterService.submitStateUpdateTask("put-lifecycle-" + request.getPolicy().getName(),
10199
new AckedClusterStateUpdateTask<Response>(request, listener) {

0 commit comments

Comments
 (0)