From 6279b00260b7a8fe71684ab82e51fc507631c87d Mon Sep 17 00:00:00 2001 From: Ashutosh0x Date: Sat, 30 May 2026 12:58:29 +0530 Subject: [PATCH] fix(security): add class loading allowlist to prevent arbitrary code execution from YAML configs Add package-level allowlist validation for all dynamic class loading paths in ToolResolver and ComponentRegistry to prevent arbitrary class instantiation via malicious YAML agent configurations. Vulnerability (Java equivalent of CVE-2026-4810): ToolResolver.resolveToolFromClass(), resolveToolsetFromClass(), resolveInstanceViaReflection(), and resolveToolsetInstanceViaReflection() all call Thread.currentThread().getContextClassLoader().loadClass() with class names directly from YAML config, with no validation on which packages can be loaded. An attacker can specify any class on the classpath (e.g., java.lang.Runtime, java.lang.ProcessBuilder) to achieve arbitrary code execution. Fix: 1. Add ALLOWED_CLASS_PREFIXES allowlist (com.google.adk., google.adk.) to restrict dynamic class loading to trusted ADK packages only 2. Add isAllowedClassForLoading() validation before every loadClass() call 3. Remove dangerous setAccessible(true) that bypasses access controls 4. Log blocked attempts at WARN level for security monitoring --- .../com/google/adk/agents/ToolResolver.java | 55 ++++++++++++++++++- .../google/adk/utils/ComponentRegistry.java | 26 +++++++++ .../google/adk/agents/ToolResolverTest.java | 4 +- 3 files changed, 82 insertions(+), 3 deletions(-) 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..b08e67195 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,35 @@ 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 +300,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 +380,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 +436,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 +481,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 +538,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..4aaf574df 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)) { diff --git a/core/src/test/java/com/google/adk/agents/ToolResolverTest.java b/core/src/test/java/com/google/adk/agents/ToolResolverTest.java index b4bd70491..1b65d9069 100644 --- a/core/src/test/java/com/google/adk/agents/ToolResolverTest.java +++ b/core/src/test/java/com/google/adk/agents/ToolResolverTest.java @@ -149,7 +149,7 @@ public void testResolveToolsetInstanceViaReflection_fieldNotFound_returnsNull() @Test public void testResolveToolsetInstanceViaReflection_classNotFound_throwsException() { - String toolsetName = "com.nonexistent.package.NonExistentClass.FIELD"; + String toolsetName = "com.google.adk.nonexistent.NonExistentClass.FIELD"; assertThrows( ClassNotFoundException.class, @@ -194,7 +194,7 @@ public void testResolveInstanceViaReflection_fieldNotFound_returnsNull() throws @Test public void testResolveInstanceViaReflection_classNotFound_throwsException() { - String toolName = "com.nonexistent.package.NonExistentClass.FIELD"; + String toolName = "com.google.adk.nonexistent.NonExistentClass.FIELD"; assertThrows( ClassNotFoundException.class, () -> ToolResolver.resolveInstanceViaReflection(toolName));