diff --git a/core/src/main/java/com/google/adk/agents/ToolResolver.java b/core/src/main/java/com/google/adk/agents/ToolResolver.java
index 09a3d79c1..2e82961e8 100644
--- a/core/src/main/java/com/google/adk/agents/ToolResolver.java
+++ b/core/src/main/java/com/google/adk/agents/ToolResolver.java
@@ -25,6 +25,7 @@
import com.google.adk.tools.BaseToolset;
import com.google.adk.utils.ComponentRegistry;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -41,6 +42,37 @@ final class ToolResolver {
private static final Logger logger = LoggerFactory.getLogger(LlmAgent.class);
+ /**
+ * Allowlist of trusted package prefixes for dynamic class loading from YAML configs.
+ *
+ *
Security: Only classes from these packages can be loaded via reflection when specified
+ * in YAML agent configurations. This prevents arbitrary class loading attacks where a
+ * malicious YAML config could specify dangerous classes (e.g., Runtime, ProcessBuilder)
+ * to achieve code execution. This is the Java equivalent of CVE-2026-4810 in adk-python.
+ */
+ private static final ImmutableSet ALLOWED_CLASS_PREFIXES =
+ ImmutableSet.of(
+ "com.google.adk.",
+ "google.adk.");
+
+ /**
+ * Validates that a class name is from an allowed package before dynamic loading.
+ *
+ * @param className the fully qualified class name to validate
+ * @return true if the class is from an allowed package
+ */
+ static boolean isAllowedClassForLoading(String className) {
+ if (isNullOrEmpty(className)) {
+ return false;
+ }
+ for (String prefix : ALLOWED_CLASS_PREFIXES) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private ToolResolver() {}
/**
@@ -270,6 +302,11 @@ static BaseToolset resolveToolsetFromClass(
if (toolsetClassOpt.isPresent()) {
toolsetClass = toolsetClassOpt.get();
} else if (isJavaQualifiedName(className)) {
+ // Security: Only allow class loading from trusted ADK packages
+ if (!isAllowedClassForLoading(className)) {
+ logger.warn("Blocked dynamic class loading from untrusted package: {}", className);
+ return null;
+ }
// Try reflection to get class
try {
Class> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
@@ -345,6 +382,12 @@ static BaseToolset resolveToolsetInstanceViaReflection(String toolsetName)
String className = toolsetName.substring(0, lastDotIndex);
String fieldName = toolsetName.substring(lastDotIndex + 1);
+ // Security: Only allow class loading from trusted ADK packages
+ if (!isAllowedClassForLoading(className)) {
+ logger.warn("Blocked dynamic class loading from untrusted package: {}", className);
+ return null;
+ }
+
Class> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
try {
@@ -395,6 +438,11 @@ static BaseTool resolveToolFromClass(String className, ToolArgsConfig args, Stri
if (classOpt.isPresent()) {
toolClass = classOpt.get();
} else if (isJavaQualifiedName(className)) {
+ // Security: Only allow class loading from trusted ADK packages
+ if (!isAllowedClassForLoading(className)) {
+ logger.warn("Blocked dynamic class loading from untrusted package: {}", className);
+ return null;
+ }
// Try reflection to get class
try {
Class> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
@@ -435,7 +483,8 @@ static BaseTool resolveToolFromClass(String className, ToolArgsConfig args, Stri
// No args provided or empty args, try default constructor
try {
Constructor extends BaseTool> constructor = toolClass.getDeclaredConstructor();
- constructor.setAccessible(true);
+ // Security: Do not call setAccessible(true) — only use public constructors
+ // to prevent bypassing access controls on non-public classes.
return constructor.newInstance();
} catch (NoSuchMethodException e) {
throw new ConfigurationException(
@@ -491,6 +540,12 @@ static BaseTool resolveInstanceViaReflection(String toolName)
String className = toolName.substring(0, lastDotIndex);
String fieldName = toolName.substring(lastDotIndex + 1);
+ // Security: Only allow class loading from trusted ADK packages
+ if (!isAllowedClassForLoading(className)) {
+ logger.warn("Blocked dynamic class loading from untrusted package: {}", className);
+ return null;
+ }
+
Class> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
try {
diff --git a/core/src/main/java/com/google/adk/utils/ComponentRegistry.java b/core/src/main/java/com/google/adk/utils/ComponentRegistry.java
index 3b2d0d14a..8d4484fa9 100644
--- a/core/src/main/java/com/google/adk/utils/ComponentRegistry.java
+++ b/core/src/main/java/com/google/adk/utils/ComponentRegistry.java
@@ -437,7 +437,33 @@ private static Optional> getType(String name, Class ty
.map(clazz -> clazz.asSubclass(type));
}
+ /**
+ * Allowlist of trusted package prefixes for dynamic class loading.
+ *
+ * Security: Only classes from these packages can be loaded via reflection when specified
+ * in YAML agent configurations. This prevents arbitrary class loading attacks (CVE-2026-4810).
+ */
+ private static final Set ALLOWED_CLASS_PREFIXES =
+ Set.of("com.google.adk.", "google.adk.");
+
+ private static boolean isAllowedClassForLoading(String className) {
+ if (isNullOrEmpty(className)) {
+ return false;
+ }
+ for (String prefix : ALLOWED_CLASS_PREFIXES) {
+ if (className.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static Optional> loadToolsetClass(String className) {
+ // Security: Only allow class loading from trusted ADK packages
+ if (!isAllowedClassForLoading(className)) {
+ logger.warn("Blocked dynamic class loading from untrusted package: {}", className);
+ return Optional.empty();
+ }
try {
Class> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
if (BaseToolset.class.isAssignableFrom(clazz)) {