Skip to content

Commit 0c48a96

Browse files
authored
[Entitlements] Validation checks on paths (#126852) (#127057)
With this PR we restrict the paths we allow access to, forbidding plugins to specify/request entitlements for reading or writing to specific protected directories. I added this validation to EntitlementInitialization, as I wanted to fail fast and this is the earliest occurrence where we have all we need: PathLookup to resolve relative paths, policies (for plugins, server, agents) and the Paths for the specific directories we want to protect. Relates to ES-10918
1 parent 3a81213 commit 0c48a96

File tree

8 files changed

+309
-24
lines changed

8 files changed

+309
-24
lines changed

docs/changelog/126852.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 126852
2+
summary: "Validation checks on paths allowed for 'files' entitlements. Restrict the paths we allow access to, forbidding plugins to specify/request entitlements for reading or writing to specific protected directories."
3+
area: Infra/Core
4+
type: enhancement
5+
issues: []

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

+88
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import org.elasticsearch.core.Booleans;
1313
import org.elasticsearch.core.PathUtils;
14+
import org.elasticsearch.core.Strings;
1415
import org.elasticsearch.core.internal.provider.ProviderLocator;
1516
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
1617
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
@@ -20,6 +21,7 @@
2021
import org.elasticsearch.entitlement.instrumentation.MethodKey;
2122
import org.elasticsearch.entitlement.instrumentation.Transformer;
2223
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
24+
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree;
2325
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
2426
import org.elasticsearch.entitlement.runtime.policy.Policy;
2527
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
@@ -56,6 +58,7 @@
5658
import java.nio.file.attribute.FileAttribute;
5759
import java.nio.file.spi.FileSystemProvider;
5860
import java.util.ArrayList;
61+
import java.util.Arrays;
5962
import java.util.Collections;
6063
import java.util.HashMap;
6164
import java.util.List;
@@ -316,6 +319,16 @@ private static PolicyManager createPolicyManager() {
316319
)
317320
)
318321
);
322+
323+
validateFilesEntitlements(
324+
pluginPolicies,
325+
pathLookup,
326+
bootstrapArgs.configDir(),
327+
bootstrapArgs.pluginsDir(),
328+
bootstrapArgs.modulesDir(),
329+
bootstrapArgs.libDir()
330+
);
331+
319332
return new PolicyManager(
320333
serverPolicy,
321334
agentEntitlements,
@@ -329,6 +342,81 @@ private static PolicyManager createPolicyManager() {
329342
);
330343
}
331344

345+
private static Set<Path> pathSet(Path... paths) {
346+
return Arrays.stream(paths).map(x -> x.toAbsolutePath().normalize()).collect(Collectors.toUnmodifiableSet());
347+
}
348+
349+
// package visible for tests
350+
static void validateFilesEntitlements(
351+
Map<String, Policy> pluginPolicies,
352+
PathLookup pathLookup,
353+
Path configDir,
354+
Path pluginsDir,
355+
Path modulesDir,
356+
Path libDir
357+
) {
358+
var readAccessForbidden = pathSet(pluginsDir, modulesDir, libDir);
359+
var writeAccessForbidden = pathSet(configDir);
360+
for (var pluginPolicy : pluginPolicies.entrySet()) {
361+
for (var scope : pluginPolicy.getValue().scopes()) {
362+
var filesEntitlement = scope.entitlements()
363+
.stream()
364+
.filter(x -> x instanceof FilesEntitlement)
365+
.map(x -> ((FilesEntitlement) x))
366+
.findFirst();
367+
if (filesEntitlement.isPresent()) {
368+
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, null);
369+
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
370+
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
371+
}
372+
}
373+
}
374+
}
375+
376+
private static IllegalArgumentException buildValidationException(
377+
String componentName,
378+
String moduleName,
379+
Path forbiddenPath,
380+
FilesEntitlement.Mode mode
381+
) {
382+
return new IllegalArgumentException(
383+
Strings.format(
384+
"policy for module [%s] in [%s] has an invalid file entitlement. Any path under [%s] is forbidden for mode [%s].",
385+
moduleName,
386+
componentName,
387+
forbiddenPath,
388+
mode
389+
)
390+
);
391+
}
392+
393+
private static void validateReadFilesEntitlements(
394+
String componentName,
395+
String moduleName,
396+
FileAccessTree fileAccessTree,
397+
Set<Path> readForbiddenPaths
398+
) {
399+
400+
for (Path forbiddenPath : readForbiddenPaths) {
401+
if (fileAccessTree.canRead(forbiddenPath)) {
402+
throw buildValidationException(componentName, moduleName, forbiddenPath, READ);
403+
}
404+
}
405+
}
406+
407+
private static void validateWriteFilesEntitlements(
408+
String componentName,
409+
String moduleName,
410+
FileAccessTree fileAccessTree,
411+
Set<Path> writeForbiddenPaths
412+
) {
413+
for (Path forbiddenPath : writeForbiddenPaths) {
414+
if (fileAccessTree.canWrite(forbiddenPath)) {
415+
throw buildValidationException(componentName, moduleName, forbiddenPath, READ_WRITE);
416+
}
417+
}
418+
}
419+
332420
private static Path getUserHome() {
333421
String userHome = System.getProperty("user.home");
334422
if (userHome == null) {

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java

+26-10
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,9 @@ static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
111111
private final String[] readPaths;
112112
private final String[] writePaths;
113113

114-
private FileAccessTree(
114+
private static String[] buildUpdatedAndSortedExclusivePaths(
115115
String componentName,
116116
String moduleName,
117-
FilesEntitlement filesEntitlement,
118-
PathLookup pathLookup,
119-
Path componentPath,
120117
List<ExclusivePath> exclusivePaths
121118
) {
122119
List<String> updatedExclusivePaths = new ArrayList<>();
@@ -125,7 +122,11 @@ private FileAccessTree(
125122
updatedExclusivePaths.add(exclusivePath.path());
126123
}
127124
}
125+
updatedExclusivePaths.sort(PATH_ORDER);
126+
return updatedExclusivePaths.toArray(new String[0]);
127+
}
128128

129+
private FileAccessTree(FilesEntitlement filesEntitlement, PathLookup pathLookup, Path componentPath, String[] sortedExclusivePaths) {
129130
List<String> readPaths = new ArrayList<>();
130131
List<String> writePaths = new ArrayList<>();
131132
BiConsumer<Path, Mode> addPath = (path, mode) -> {
@@ -177,11 +178,10 @@ private FileAccessTree(
177178
Path jdk = Paths.get(System.getProperty("java.home"));
178179
addPathAndMaybeLink.accept(jdk.resolve("conf"), Mode.READ);
179180

180-
updatedExclusivePaths.sort(PATH_ORDER);
181181
readPaths.sort(PATH_ORDER);
182182
writePaths.sort(PATH_ORDER);
183183

184-
this.exclusivePaths = updatedExclusivePaths.toArray(new String[0]);
184+
this.exclusivePaths = sortedExclusivePaths;
185185
this.readPaths = pruneSortedPaths(readPaths).toArray(new String[0]);
186186
this.writePaths = pruneSortedPaths(writePaths).toArray(new String[0]);
187187
}
@@ -203,22 +203,38 @@ static List<String> pruneSortedPaths(List<String> paths) {
203203
return prunedReadPaths;
204204
}
205205

206-
public static FileAccessTree of(
206+
static FileAccessTree of(
207207
String componentName,
208208
String moduleName,
209209
FilesEntitlement filesEntitlement,
210210
PathLookup pathLookup,
211211
@Nullable Path componentPath,
212212
List<ExclusivePath> exclusivePaths
213213
) {
214-
return new FileAccessTree(componentName, moduleName, filesEntitlement, pathLookup, componentPath, exclusivePaths);
214+
return new FileAccessTree(
215+
filesEntitlement,
216+
pathLookup,
217+
componentPath,
218+
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths)
219+
);
220+
}
221+
222+
/**
223+
* A special factory method to create a FileAccessTree with no ExclusivePaths, e.g. for quick validation or for default file access
224+
*/
225+
public static FileAccessTree withoutExclusivePaths(
226+
FilesEntitlement filesEntitlement,
227+
PathLookup pathLookup,
228+
@Nullable Path componentPath
229+
) {
230+
return new FileAccessTree(filesEntitlement, pathLookup, componentPath, new String[0]);
215231
}
216232

217-
boolean canRead(Path path) {
233+
public boolean canRead(Path path) {
218234
return checkPath(normalizePath(path), readPaths);
219235
}
220236

221-
boolean canWrite(Path path) {
237+
public boolean canWrite(Path path) {
222238
return checkPath(normalizePath(path), writePaths);
223239
}
224240

libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java

+3-8
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,13 @@ public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementCla
101101
}
102102
}
103103

104-
private FileAccessTree getDefaultFileAccess(String componentName, Path componentPath) {
105-
return FileAccessTree.of(componentName, UNKNOWN_COMPONENT_NAME, FilesEntitlement.EMPTY, pathLookup, componentPath, List.of());
104+
private FileAccessTree getDefaultFileAccess(Path componentPath) {
105+
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPath);
106106
}
107107

108108
// pkg private for testing
109109
ModuleEntitlements defaultEntitlements(String componentName, Path componentPath, String moduleName) {
110-
return new ModuleEntitlements(
111-
componentName,
112-
Map.of(),
113-
getDefaultFileAccess(componentName, componentPath),
114-
getLogger(componentName, moduleName)
115-
);
110+
return new ModuleEntitlements(componentName, Map.of(), getDefaultFileAccess(componentPath), getLogger(componentName, moduleName));
116111
}
117112

118113
// pkg private for testing

0 commit comments

Comments
 (0)