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 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)) {