Skip to content

[9.0] [Entitlements] Validation checks on paths (#126852) #127057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/126852.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 126852
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."
area: Infra/Core
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.elasticsearch.core.Booleans;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.Strings;
import org.elasticsearch.core.internal.provider.ProviderLocator;
import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap;
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
Expand All @@ -20,6 +21,7 @@
import org.elasticsearch.entitlement.instrumentation.MethodKey;
import org.elasticsearch.entitlement.instrumentation.Transformer;
import org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.FileAccessTree;
import org.elasticsearch.entitlement.runtime.policy.PathLookup;
import org.elasticsearch.entitlement.runtime.policy.Policy;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
Expand Down Expand Up @@ -56,6 +58,7 @@
import java.nio.file.attribute.FileAttribute;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -316,6 +319,16 @@ private static PolicyManager createPolicyManager() {
)
)
);

validateFilesEntitlements(
pluginPolicies,
pathLookup,
bootstrapArgs.configDir(),
bootstrapArgs.pluginsDir(),
bootstrapArgs.modulesDir(),
bootstrapArgs.libDir()
);

return new PolicyManager(
serverPolicy,
agentEntitlements,
Expand All @@ -329,6 +342,81 @@ private static PolicyManager createPolicyManager() {
);
}

private static Set<Path> pathSet(Path... paths) {
return Arrays.stream(paths).map(x -> x.toAbsolutePath().normalize()).collect(Collectors.toUnmodifiableSet());
}

// package visible for tests
static void validateFilesEntitlements(
Map<String, Policy> pluginPolicies,
PathLookup pathLookup,
Path configDir,
Path pluginsDir,
Path modulesDir,
Path libDir
) {
var readAccessForbidden = pathSet(pluginsDir, modulesDir, libDir);
var writeAccessForbidden = pathSet(configDir);
for (var pluginPolicy : pluginPolicies.entrySet()) {
for (var scope : pluginPolicy.getValue().scopes()) {
var filesEntitlement = scope.entitlements()
.stream()
.filter(x -> x instanceof FilesEntitlement)
.map(x -> ((FilesEntitlement) x))
.findFirst();
if (filesEntitlement.isPresent()) {
var fileAccessTree = FileAccessTree.withoutExclusivePaths(filesEntitlement.get(), pathLookup, null);
validateReadFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, readAccessForbidden);
validateWriteFilesEntitlements(pluginPolicy.getKey(), scope.moduleName(), fileAccessTree, writeAccessForbidden);
}
}
}
}

private static IllegalArgumentException buildValidationException(
String componentName,
String moduleName,
Path forbiddenPath,
FilesEntitlement.Mode mode
) {
return new IllegalArgumentException(
Strings.format(
"policy for module [%s] in [%s] has an invalid file entitlement. Any path under [%s] is forbidden for mode [%s].",
moduleName,
componentName,
forbiddenPath,
mode
)
);
}

private static void validateReadFilesEntitlements(
String componentName,
String moduleName,
FileAccessTree fileAccessTree,
Set<Path> readForbiddenPaths
) {

for (Path forbiddenPath : readForbiddenPaths) {
if (fileAccessTree.canRead(forbiddenPath)) {
throw buildValidationException(componentName, moduleName, forbiddenPath, READ);
}
}
}

private static void validateWriteFilesEntitlements(
String componentName,
String moduleName,
FileAccessTree fileAccessTree,
Set<Path> writeForbiddenPaths
) {
for (Path forbiddenPath : writeForbiddenPaths) {
if (fileAccessTree.canWrite(forbiddenPath)) {
throw buildValidationException(componentName, moduleName, forbiddenPath, READ_WRITE);
}
}
}

private static Path getUserHome() {
String userHome = System.getProperty("user.home");
if (userHome == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,9 @@ static void validateExclusivePaths(List<ExclusivePath> exclusivePaths) {
private final String[] readPaths;
private final String[] writePaths;

private FileAccessTree(
private static String[] buildUpdatedAndSortedExclusivePaths(
String componentName,
String moduleName,
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
Path componentPath,
List<ExclusivePath> exclusivePaths
) {
List<String> updatedExclusivePaths = new ArrayList<>();
Expand All @@ -125,7 +122,11 @@ private FileAccessTree(
updatedExclusivePaths.add(exclusivePath.path());
}
}
updatedExclusivePaths.sort(PATH_ORDER);
return updatedExclusivePaths.toArray(new String[0]);
}

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

updatedExclusivePaths.sort(PATH_ORDER);
readPaths.sort(PATH_ORDER);
writePaths.sort(PATH_ORDER);

this.exclusivePaths = updatedExclusivePaths.toArray(new String[0]);
this.exclusivePaths = sortedExclusivePaths;
this.readPaths = pruneSortedPaths(readPaths).toArray(new String[0]);
this.writePaths = pruneSortedPaths(writePaths).toArray(new String[0]);
}
Expand All @@ -203,22 +203,38 @@ static List<String> pruneSortedPaths(List<String> paths) {
return prunedReadPaths;
}

public static FileAccessTree of(
static FileAccessTree of(
String componentName,
String moduleName,
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
@Nullable Path componentPath,
List<ExclusivePath> exclusivePaths
) {
return new FileAccessTree(componentName, moduleName, filesEntitlement, pathLookup, componentPath, exclusivePaths);
return new FileAccessTree(
filesEntitlement,
pathLookup,
componentPath,
buildUpdatedAndSortedExclusivePaths(componentName, moduleName, exclusivePaths)
);
}

/**
* A special factory method to create a FileAccessTree with no ExclusivePaths, e.g. for quick validation or for default file access
*/
public static FileAccessTree withoutExclusivePaths(
FilesEntitlement filesEntitlement,
PathLookup pathLookup,
@Nullable Path componentPath
) {
return new FileAccessTree(filesEntitlement, pathLookup, componentPath, new String[0]);
}

boolean canRead(Path path) {
public boolean canRead(Path path) {
return checkPath(normalizePath(path), readPaths);
}

boolean canWrite(Path path) {
public boolean canWrite(Path path) {
return checkPath(normalizePath(path), writePaths);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,13 @@ public <E extends Entitlement> Stream<E> getEntitlements(Class<E> entitlementCla
}
}

private FileAccessTree getDefaultFileAccess(String componentName, Path componentPath) {
return FileAccessTree.of(componentName, UNKNOWN_COMPONENT_NAME, FilesEntitlement.EMPTY, pathLookup, componentPath, List.of());
private FileAccessTree getDefaultFileAccess(Path componentPath) {
return FileAccessTree.withoutExclusivePaths(FilesEntitlement.EMPTY, pathLookup, componentPath);
}

// pkg private for testing
ModuleEntitlements defaultEntitlements(String componentName, Path componentPath, String moduleName) {
return new ModuleEntitlements(
componentName,
Map.of(),
getDefaultFileAccess(componentName, componentPath),
getLogger(componentName, moduleName)
);
return new ModuleEntitlements(componentName, Map.of(), getDefaultFileAccess(componentPath), getLogger(componentName, moduleName));
}

// pkg private for testing
Expand Down
Loading