Skip to content
Open
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
57 changes: 56 additions & 1 deletion core/src/main/java/com/google/adk/agents/ToolResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
* <p>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<String> 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() {}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/java/com/google/adk/utils/ComponentRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,33 @@ private static <T> Optional<Class<? extends T>> getType(String name, Class<T> ty
.map(clazz -> clazz.asSubclass(type));
}

/**
* Allowlist of trusted package prefixes for dynamic class loading.
*
* <p>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<String> 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<Class<? extends BaseToolset>> 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)) {
Expand Down