From b39ef36724f3cba538e7fe3d18e025d6657e8c05 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 | 57 ++++++++++++++++++- .../google/adk/utils/ComponentRegistry.java | 26 +++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) 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)) {